diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 00000000..9df388d7 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 7b8e22ad1a4a25c2a4b6733e51a905c1 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/about/changelog.doctree b/.doctrees/about/changelog.doctree new file mode 100644 index 00000000..a10f31be Binary files /dev/null and b/.doctrees/about/changelog.doctree differ diff --git a/.doctrees/about/license.doctree b/.doctrees/about/license.doctree new file mode 100644 index 00000000..e0dda466 Binary files /dev/null and b/.doctrees/about/license.doctree differ diff --git a/.doctrees/dev/contributing.doctree b/.doctrees/dev/contributing.doctree new file mode 100644 index 00000000..5a140d64 Binary files /dev/null and b/.doctrees/dev/contributing.doctree differ diff --git a/.doctrees/dev/dev_installation.doctree b/.doctrees/dev/dev_installation.doctree new file mode 100644 index 00000000..6004178e Binary files /dev/null and b/.doctrees/dev/dev_installation.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 00000000..4a96ae84 Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/fundamentals/index.doctree b/.doctrees/fundamentals/index.doctree new file mode 100644 index 00000000..55c67e82 Binary files /dev/null and b/.doctrees/fundamentals/index.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 00000000..cc918011 Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/installation/index.doctree b/.doctrees/installation/index.doctree new file mode 100644 index 00000000..4fb7f83b Binary files /dev/null and b/.doctrees/installation/index.doctree differ diff --git a/.doctrees/reference/cli.doctree b/.doctrees/reference/cli.doctree new file mode 100644 index 00000000..48ed98d7 Binary files /dev/null and b/.doctrees/reference/cli.doctree differ diff --git a/.doctrees/reference/cli_subcommands/createinputs.doctree b/.doctrees/reference/cli_subcommands/createinputs.doctree new file mode 100644 index 00000000..f3836c72 Binary files /dev/null and b/.doctrees/reference/cli_subcommands/createinputs.doctree differ diff --git a/.doctrees/reference/cli_subcommands/description.doctree b/.doctrees/reference/cli_subcommands/description.doctree new file mode 100644 index 00000000..7f2dcf97 Binary files /dev/null and b/.doctrees/reference/cli_subcommands/description.doctree differ diff --git a/.doctrees/reference/cli_subcommands/descriptionquality.doctree b/.doctrees/reference/cli_subcommands/descriptionquality.doctree new file mode 100644 index 00000000..0d7947af Binary files /dev/null and b/.doctrees/reference/cli_subcommands/descriptionquality.doctree differ diff --git a/.doctrees/reference/cli_subcommands/plot.doctree b/.doctrees/reference/cli_subcommands/plot.doctree new file mode 100644 index 00000000..d54866ee Binary files /dev/null and b/.doctrees/reference/cli_subcommands/plot.doctree differ diff --git a/.doctrees/reference/cli_subcommands/plotauto.doctree b/.doctrees/reference/cli_subcommands/plotauto.doctree new file mode 100644 index 00000000..e9bafc80 Binary files /dev/null and b/.doctrees/reference/cli_subcommands/plotauto.doctree differ diff --git a/.doctrees/reference/cli_subcommands/plotautoia.doctree b/.doctrees/reference/cli_subcommands/plotautoia.doctree new file mode 100644 index 00000000..f5e4e1f1 Binary files /dev/null and b/.doctrees/reference/cli_subcommands/plotautoia.doctree differ diff --git a/.doctrees/reference/cli_subcommands/plotdos.doctree b/.doctrees/reference/cli_subcommands/plotdos.doctree new file mode 100644 index 00000000..dfdddc7a Binary files /dev/null and b/.doctrees/reference/cli_subcommands/plotdos.doctree differ diff --git a/.doctrees/reference/cli_subcommands/ploticohpdistance.doctree b/.doctrees/reference/cli_subcommands/ploticohpdistance.doctree new file mode 100644 index 00000000..65e4b071 Binary files /dev/null and b/.doctrees/reference/cli_subcommands/ploticohpdistance.doctree differ diff --git a/.doctrees/reference/index.doctree b/.doctrees/reference/index.doctree new file mode 100644 index 00000000..1c1a0b68 Binary files /dev/null and b/.doctrees/reference/index.doctree differ diff --git a/.doctrees/reference/lobsterpy.cohp.analyze.Analysis.doctree b/.doctrees/reference/lobsterpy.cohp.analyze.Analysis.doctree new file mode 100644 index 00000000..db1aadbd Binary files /dev/null and b/.doctrees/reference/lobsterpy.cohp.analyze.Analysis.doctree differ diff --git a/.doctrees/reference/lobsterpy.cohp.analyze.doctree b/.doctrees/reference/lobsterpy.cohp.analyze.doctree new file mode 100644 index 00000000..4ca9ce3d Binary files /dev/null and b/.doctrees/reference/lobsterpy.cohp.analyze.doctree differ diff --git a/.doctrees/reference/lobsterpy.cohp.describe.Description.doctree b/.doctrees/reference/lobsterpy.cohp.describe.Description.doctree new file mode 100644 index 00000000..d7080178 Binary files /dev/null and b/.doctrees/reference/lobsterpy.cohp.describe.Description.doctree differ diff --git a/.doctrees/reference/lobsterpy.cohp.describe.doctree b/.doctrees/reference/lobsterpy.cohp.describe.doctree new file mode 100644 index 00000000..8a8244c0 Binary files /dev/null and b/.doctrees/reference/lobsterpy.cohp.describe.doctree differ diff --git a/.doctrees/reference/lobsterpy.cohp.doctree b/.doctrees/reference/lobsterpy.cohp.doctree new file mode 100644 index 00000000..9f91cffd Binary files /dev/null and b/.doctrees/reference/lobsterpy.cohp.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.batch.BatchCoxxFingerprint.doctree b/.doctrees/reference/lobsterpy.featurize.batch.BatchCoxxFingerprint.doctree new file mode 100644 index 00000000..e8093c4d Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.batch.BatchCoxxFingerprint.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.batch.BatchDosFeaturizer.doctree b/.doctrees/reference/lobsterpy.featurize.batch.BatchDosFeaturizer.doctree new file mode 100644 index 00000000..929dd41b Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.batch.BatchDosFeaturizer.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.batch.BatchStructureGraphs.doctree b/.doctrees/reference/lobsterpy.featurize.batch.BatchStructureGraphs.doctree new file mode 100644 index 00000000..b4b672f6 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.batch.BatchStructureGraphs.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.batch.BatchSummaryFeaturizer.doctree b/.doctrees/reference/lobsterpy.featurize.batch.BatchSummaryFeaturizer.doctree new file mode 100644 index 00000000..d3034f93 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.batch.BatchSummaryFeaturizer.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.batch.doctree b/.doctrees/reference/lobsterpy.featurize.batch.doctree new file mode 100644 index 00000000..0f454edf Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.batch.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.CoxxFingerprint.doctree b/.doctrees/reference/lobsterpy.featurize.core.CoxxFingerprint.doctree new file mode 100644 index 00000000..f72c07a8 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.CoxxFingerprint.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.DosFingerprint.doctree b/.doctrees/reference/lobsterpy.featurize.core.DosFingerprint.doctree new file mode 100644 index 00000000..89233a3b Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.DosFingerprint.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.FeaturizeCOXX.doctree b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeCOXX.doctree new file mode 100644 index 00000000..3a63bcfd Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeCOXX.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.FeaturizeCharges.doctree b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeCharges.doctree new file mode 100644 index 00000000..e2793ff4 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeCharges.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.FeaturizeDoscar.doctree b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeDoscar.doctree new file mode 100644 index 00000000..d2a0bd8f Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeDoscar.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.FeaturizeLobsterpy.doctree b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeLobsterpy.doctree new file mode 100644 index 00000000..ca761b9c Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.FeaturizeLobsterpy.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.core.doctree b/.doctrees/reference/lobsterpy.featurize.core.doctree new file mode 100644 index 00000000..168f8274 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.core.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.doctree b/.doctrees/reference/lobsterpy.featurize.doctree new file mode 100644 index 00000000..b5e0433e Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.utils.doctree b/.doctrees/reference/lobsterpy.featurize.utils.doctree new file mode 100644 index 00000000..f08edfb5 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.utils.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.utils.get_file_paths.doctree b/.doctrees/reference/lobsterpy.featurize.utils.get_file_paths.doctree new file mode 100644 index 00000000..ff7e1ea9 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.utils.get_file_paths.doctree differ diff --git a/.doctrees/reference/lobsterpy.featurize.utils.get_structure_path.doctree b/.doctrees/reference/lobsterpy.featurize.utils.get_structure_path.doctree new file mode 100644 index 00000000..88ccab53 Binary files /dev/null and b/.doctrees/reference/lobsterpy.featurize.utils.get_structure_path.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.IcohpDistancePlotter.doctree b/.doctrees/reference/lobsterpy.plotting.IcohpDistancePlotter.doctree new file mode 100644 index 00000000..6e1d9077 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.IcohpDistancePlotter.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.InteractiveCohpPlotter.doctree b/.doctrees/reference/lobsterpy.plotting.InteractiveCohpPlotter.doctree new file mode 100644 index 00000000..0b9a251d Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.InteractiveCohpPlotter.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.PlainCohpPlotter.doctree b/.doctrees/reference/lobsterpy.plotting.PlainCohpPlotter.doctree new file mode 100644 index 00000000..ec58299e Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.PlainCohpPlotter.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.PlainDosPlotter.doctree b/.doctrees/reference/lobsterpy.plotting.PlainDosPlotter.doctree new file mode 100644 index 00000000..70bdeab1 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.PlainDosPlotter.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.TYPE_CHECKING.doctree b/.doctrees/reference/lobsterpy.plotting.TYPE_CHECKING.doctree new file mode 100644 index 00000000..eb5846a2 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.TYPE_CHECKING.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.doctree b/.doctrees/reference/lobsterpy.plotting.doctree new file mode 100644 index 00000000..806d60ff Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.get_style_list.doctree b/.doctrees/reference/lobsterpy.plotting.get_style_list.doctree new file mode 100644 index 00000000..925b7ccc Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.get_style_list.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.cohp_axis_style_dict.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.cohp_axis_style_dict.doctree new file mode 100644 index 00000000..07c6379b Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.cohp_axis_style_dict.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.doctree new file mode 100644 index 00000000..57e90c50 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.energy_axis_style_dict.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.energy_axis_style_dict.doctree new file mode 100644 index 00000000..1d86660e Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.energy_axis_style_dict.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.layout_dict.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.layout_dict.doctree new file mode 100644 index 00000000..d3ce7aa8 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.layout_dict.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.legend_style_dict.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.legend_style_dict.doctree new file mode 100644 index 00000000..f29dbe5c Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.legend_style_dict.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.spin_down_trace_style_dict.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.spin_down_trace_style_dict.doctree new file mode 100644 index 00000000..bac824bf Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.spin_down_trace_style_dict.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.layout_dicts.spin_up_trace_style_dict.doctree b/.doctrees/reference/lobsterpy.plotting.layout_dicts.spin_up_trace_style_dict.doctree new file mode 100644 index 00000000..a7e18f14 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.layout_dicts.spin_up_trace_style_dict.doctree differ diff --git a/.doctrees/reference/lobsterpy.structuregraph.doctree b/.doctrees/reference/lobsterpy.structuregraph.doctree new file mode 100644 index 00000000..71bc722b Binary files /dev/null and b/.doctrees/reference/lobsterpy.structuregraph.doctree differ diff --git a/.doctrees/reference/lobsterpy.structuregraph.graph.LobsterGraph.doctree b/.doctrees/reference/lobsterpy.structuregraph.graph.LobsterGraph.doctree new file mode 100644 index 00000000..e9e2b8e9 Binary files /dev/null and b/.doctrees/reference/lobsterpy.structuregraph.graph.LobsterGraph.doctree differ diff --git a/.doctrees/reference/lobsterpy.structuregraph.graph.TYPE_CHECKING.doctree b/.doctrees/reference/lobsterpy.structuregraph.graph.TYPE_CHECKING.doctree new file mode 100644 index 00000000..3d24578f Binary files /dev/null and b/.doctrees/reference/lobsterpy.structuregraph.graph.TYPE_CHECKING.doctree differ diff --git a/.doctrees/reference/lobsterpy.structuregraph.graph.doctree b/.doctrees/reference/lobsterpy.structuregraph.graph.doctree new file mode 100644 index 00000000..b1e6ff4e Binary files /dev/null and b/.doctrees/reference/lobsterpy.structuregraph.graph.doctree differ diff --git a/.doctrees/tutorial/atomateauto.doctree b/.doctrees/tutorial/atomateauto.doctree new file mode 100644 index 00000000..3b993a9d Binary files /dev/null and b/.doctrees/tutorial/atomateauto.doctree differ diff --git a/.doctrees/tutorial/commandlineinterface.doctree b/.doctrees/tutorial/commandlineinterface.doctree new file mode 100644 index 00000000..5422b3e7 Binary files /dev/null and b/.doctrees/tutorial/commandlineinterface.doctree differ diff --git a/.doctrees/tutorial/computingtimes.doctree b/.doctrees/tutorial/computingtimes.doctree new file mode 100644 index 00000000..345007d9 Binary files /dev/null and b/.doctrees/tutorial/computingtimes.doctree differ diff --git a/.doctrees/tutorial/index.doctree b/.doctrees/tutorial/index.doctree new file mode 100644 index 00000000..77563cd9 Binary files /dev/null and b/.doctrees/tutorial/index.doctree differ diff --git a/.doctrees/tutorial/tutorial.doctree b/.doctrees/tutorial/tutorial.doctree new file mode 100644 index 00000000..4f71509f Binary files /dev/null and b/.doctrees/tutorial/tutorial.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/_downloads/8f717a44707c0b40f1d30cb8055189c3/CdF2.html b/_downloads/8f717a44707c0b40f1d30cb8055189c3/CdF2.html new file mode 100644 index 00000000..fb28558c --- /dev/null +++ b/_downloads/8f717a44707c0b40f1d30cb8055189c3/CdF2.html @@ -0,0 +1,14 @@ + +
+ +
+# Copyright (c) lobsterpy development team
+# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License
+
+"""This module defines classes to analyze the COHPs automatically."""
+
+from __future__ import annotations
+
+import warnings
+from collections import Counter
+from itertools import combinations_with_replacement, permutations
+from pathlib import Path
+
+import numpy as np
+from pymatgen.analysis.bond_valence import BVAnalyzer
+from pymatgen.core.structure import Structure
+from pymatgen.electronic_structure.cohp import CompleteCohp
+from pymatgen.electronic_structure.core import Spin
+from pymatgen.electronic_structure.dos import LobsterCompleteDos
+from pymatgen.io.lobster import (
+ Bandoverlaps,
+ Charge,
+ Doscar,
+ Icohplist,
+ Lobsterin,
+ Lobsterout,
+ MadelungEnergies,
+)
+from pymatgen.io.lobster.lobsterenv import LobsterNeighbors
+from pymatgen.io.vasp.outputs import Vasprun
+from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
+
+
+
+[docs]
+class Analysis:
+ """
+ Class to automatically analyze COHP/COOP/COBI populations from Lobster.
+
+ :param are_cobis: bool indicating if file contains COBI/ICOBI data
+ :param are_coops: bool indicating if file contains COOP/ICOOP data
+ :param cutoff_icohp: Cutoff in percentage for evaluating neighbors based on ICOHP values.
+ cutoff_icohp*max_icohp limits the number of considered neighbours for evaluating environments.
+ :param path_to_cohpcar: path to `COHPCAR.lobster` or `COBICAR.lobster` or `COOPCAR.lobster` .
+ :param path_to_charge: path to `CHARGE.lobster`.
+ :param path_to_icohplist: path to `ICOHPLIST.lobster` or `ICOBILIST.lobster` or `ICOOPLIST.lobster`.
+ :param path_to_poscar: path to structure (e.g., `POSCAR` or `POSCAR.lobster`)
+ :param path_to_madelung: path to `MadelungEnergies.lobster`.
+ :param charge_obj: pymatgen lobster.io.charge object
+ :param completecohp_obj: pymatgen.electronic_structure.cohp.CompleteCohp object
+ :param icohplist_obj: pymatgen lobster.io.Icohplist object
+ :param madelung_obj: pymatgen lobster.io.MadelungEnergies object
+ :param noise_cutoff: Sets the lower limit tolerance for ICOHPs or ICOOPs or ICOBIs considered
+ in analysis.
+ :param orbital_cutoff: Sets the minimum percentage for the orbital contribution considered to be
+ relevant in orbital resolved analysis. (Affects only when orbital_resolved argument is set to True)
+ Set it to 0 to get results of all orbitals in the detected relevant bonds. Default is to 0.05 i.e.
+ only analyzes if orbital contribution is 5 % or more.
+ :param orbital_resolved: bool indicating whether orbital wise analysis is performed
+ :param type_charge: If no path_to_charge is given, Valences will be used. Otherwise, Mulliken charges.
+ Löwdin charges cannot be selected at the moment.
+ :param which_bonds: Selects kinds of bonds that are analyzed. `cation-anion` is the default.
+ Alternatively, `all` bonds can also be selected. Support to other kinds of bonds will be
+ added soon.
+ :param summed_spins: if True, COHP `Spin.up` and `Spin.down` populations will be summed
+ :param start: sets the lower limit of energy for evaluation of bonding and antibonding
+ percentages below efermi. Defaults to None (i.e., all populations below efermi are included)
+
+ Attributes:
+ - condensed_bonding_analysis: dict including a summary of the most important bonding properties
+ - final_dict_bonds: dict including information on ICOHPs per bond type
+ - final_dict_ions: dict including information on environments of cations
+ - chemenv: pymatgen.io.lobster.lobsterenv.LobsterNeighbors object
+ - lse: LightStructureEnvironment from pymatgen
+ - anion_types: Set of Element objects from pymatgen
+ - list_equivalent_sites: list of site indices of sites that indicate which sites are equivalent
+ e.g., [0 1 2 2 2] where site 0, 1, 2 indicate sites that are independent from each other
+ - seq_cohps: list of cohps
+ - seq_coord_ions: list of co-ordination environment strings for each cation
+ - seq_equivalent_sites: seq of inequivalent sites
+ - seq_ineq_ions: seq of inequivalent cations/sites in the structure
+ - seq_infos_bonds (list): information on cation anion bonds (lists
+ of pymatgen.io.lobster.lobsterenv.ICOHPNeighborsInfo)
+ - spg: space group information
+ - structure: Structure object
+
+ """
+
+ def __init__(
+ self,
+ path_to_poscar: str | Path | None,
+ path_to_icohplist: str | Path | None,
+ path_to_cohpcar: str | Path | None,
+ path_to_charge: str | Path | None = None,
+ path_to_madelung: str | Path | None = None,
+ icohplist_obj: Icohplist | None = None,
+ completecohp_obj: CompleteCohp | None = None,
+ charge_obj: Charge | None = None,
+ madelung_obj: MadelungEnergies | None = None,
+ are_cobis: bool = False,
+ are_coops: bool = False,
+ cutoff_icohp: float = 0.1,
+ noise_cutoff: float = 0.1,
+ orbital_cutoff: float = 0.05,
+ orbital_resolved: bool = False,
+ start: float | None = None,
+ summed_spins: bool = True,
+ type_charge: str | None = None,
+ which_bonds: str = "cation-anion",
+ ):
+ """
+ Initialize automatic bonding analysis.
+
+ :param are_cobis: bool indicating if file contains COBI/ICOBI data
+ :param are_coops: bool indicating if file contains COOP/ICOOP data
+ :param cutoff_icohp: Cutoff in percentage for evaluating neighbors based on ICOHP values.
+ cutoff_icohp*max_icohp limits the number of considered neighbours for evaluating environments.
+ :param path_to_cohpcar: path to `COHPCAR.lobster` or `COBICAR.lobster` or `COOPCAR.lobster` .
+ :param path_to_charge: path to `CHARGE.lobster`.
+ :param path_to_icohplist: path to `ICOHPLIST.lobster` or `ICOBILIST.lobster` or `ICOOPLIST.lobster`.
+ :param path_to_poscar: path to structure (e.g., `POSCAR` or `POSCAR.lobster`)
+ :param path_to_madelung: path to `MadelungEnergies.lobster`.
+ :param charge_obj: pymatgen lobster.io.charge object (Optional)
+ :param completecohp_obj: pymatgen.electronic_structure.cohp.CompleteCohp object
+ :param icohplist_obj: pymatgen lobster.io.Icohplist object
+ :param madelung_obj: pymatgen lobster.io.MadelungEnergies object
+ :param noise_cutoff: Sets the lower limit tolerance for ICOHPs or ICOOPs or ICOBIs considered
+ in analysis.
+ :param orbital_cutoff: Sets the minimum percentage for the orbital contribution considered to be
+ relevant in orbital resolved analysis. (Affects only when orbital_resolved argument is set to True)
+ Set it to 0 to get results of all orbitals in the detected relevant bonds. Default is to 0.05 i.e.
+ only analyzes if orbital contribution is 5 % or more.
+ :param orbital_resolved: bool indicating whether orbital wise analysis is performed
+ :param type_charge: If no path_to_charge is given, Valences will be used. Otherwise, Mulliken charges.
+ Löwdin charges cannot be selected at the moment.
+ :param which_bonds: Selects kinds of bonds that are analyzed. `cation-anion` is the default.
+ Alternatively, `all` bonds can also be selected. Support to other kinds of bonds will be
+ added soon.
+ :param summed_spins: if True, COHP `Spin.up` and `Spin.down` populations will be summed
+ :param start: sets the lower limit of energy for evaluation of bonding and antibonding
+ percentages below efermi. Defaults to None (i.e., all populations below efermi are included)
+
+ """
+ self.start = start
+ self.completecohp_obj = completecohp_obj
+ self.icohplist_obj = icohplist_obj
+ # checks to ensure LobsterEnv inputs are not duplicated in case users provide both path and obj
+ if self.completecohp_obj is not None and self.icohplist_obj is not None:
+ self.path_to_poscar = None
+ self.path_to_cohpcar = None
+ self.path_to_icohplist = None
+ else:
+ self.path_to_poscar = path_to_poscar
+ self.path_to_icohplist = path_to_icohplist
+ self.path_to_cohpcar = path_to_cohpcar
+ self.which_bonds = which_bonds
+ self.cutoff_icohp = cutoff_icohp
+ self.orbital_cutoff = orbital_cutoff
+ self.path_to_charge = path_to_charge
+ self.charge_obj = charge_obj
+ self.path_to_madelung = path_to_madelung
+ self.madelung_obj = madelung_obj
+ self.are_cobis = are_cobis
+ self.are_coops = are_coops
+ self.noise_cutoff = noise_cutoff
+ self.setup_env()
+ self.get_information_all_bonds(summed_spins=summed_spins)
+ self.orbital_resolved = orbital_resolved
+
+ # This determines how cations and anions
+ if path_to_charge is None and charge_obj is None:
+ self.type_charge = "Valences"
+ else:
+ if type_charge is None:
+ self.type_charge = "Mulliken"
+ elif type_charge == "Mulliken":
+ self.type_charge = "Mulliken"
+ elif type_charge == "Löwdin":
+ raise ValueError("Only Mulliken charges can be used here at the moment. Implementation will follow.")
+ else:
+ self.type_charge = "Valences"
+ print("type_charge cannot be read! Please use Mulliken/Löwdin. Now, we will use valences")
+
+ self.set_condensed_bonding_analysis()
+ self.set_summary_dicts()
+ self.path_to_madelung = path_to_madelung
+
+
+[docs]
+ def setup_env(self):
+ """
+ Set up the light structure environments based on COHPs using this method.
+
+ Returns:
+ None
+
+ """
+ self.structure = (
+ Structure.from_file(self.path_to_poscar) if self.path_to_poscar else self.completecohp_obj.structure
+ )
+ sga = SpacegroupAnalyzer(structure=self.structure)
+ symmetry_dataset = sga.get_symmetry_dataset()
+ equivalent_sites = symmetry_dataset["equivalent_atoms"]
+ self.list_equivalent_sites = equivalent_sites
+ self.seq_equivalent_sites = list(set(equivalent_sites))
+ self.spg = symmetry_dataset["international"]
+
+ if self.which_bonds == "cation-anion":
+ try:
+ self.chemenv = LobsterNeighbors(
+ filename_icohp=self.path_to_icohplist,
+ obj_icohp=self.icohplist_obj,
+ structure=self.structure,
+ additional_condition=1,
+ perc_strength_icohp=self.cutoff_icohp,
+ filename_charge=self.path_to_charge,
+ obj_charge=self.charge_obj,
+ valences=None,
+ valences_from_charges=True,
+ adapt_extremum_to_add_cond=True,
+ are_cobis=self.are_cobis,
+ are_coops=self.are_coops,
+ noise_cutoff=self.noise_cutoff,
+ )
+ except ValueError as err:
+ if (
+ str(err) == "min() arg is an empty sequence"
+ or str(err) == "All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work"
+ ):
+ raise ValueError(
+ "Consider switching to an analysis of all bonds and not only cation-anion bonds."
+ " It looks like no cations are detected."
+ )
+ raise err
+ elif self.which_bonds == "all":
+ # raise ValueError("only cation anion bonds implemented so far")
+ self.chemenv = LobsterNeighbors(
+ filename_icohp=self.path_to_icohplist,
+ obj_icohp=self.icohplist_obj,
+ structure=self.structure,
+ additional_condition=0,
+ perc_strength_icohp=self.cutoff_icohp,
+ filename_charge=self.path_to_charge,
+ obj_charge=self.charge_obj,
+ valences_from_charges=True,
+ adapt_extremum_to_add_cond=True,
+ are_cobis=self.are_cobis,
+ are_coops=self.are_coops,
+ noise_cutoff=self.noise_cutoff,
+ )
+
+ else:
+ raise ValueError("only cation anion and all bonds implemented so far")
+
+ # determine cations and anions
+ try:
+ if self.which_bonds == "cation-anion":
+ self.lse = self.chemenv.get_light_structure_environment(only_cation_environments=True)
+ elif self.which_bonds == "all":
+ self.lse = self.chemenv.get_light_structure_environment(only_cation_environments=False)
+ except ValueError:
+
+ class Lse:
+ """Test class when error was raised."""
+
+ def __init__(self, chemenv, valences=None):
+ """
+ Test class when error was raised.
+
+ :param chemenv: LobsterNeighbors object
+ :param valences: list of valences
+
+ """
+ if valences is None:
+ self.coordination_environments = []
+ for coord in chemenv:
+ if len(coord) > 0:
+ self.coordination_environments.append([{"ce_symbol": str(len(coord))}])
+ else:
+ self.coordination_environments.append([{"ce_symbol": None}])
+ else:
+ self.coordination_environments = []
+
+ for val, coord in zip(valences, chemenv):
+ if val >= 0.0 and len(coord) > 0:
+ self.coordination_environments.append([{"ce_symbol": str(len(coord))}])
+ else:
+ self.coordination_environments.append([{"ce_symbol": None}])
+
+ if self.which_bonds == "all":
+ self.lse = Lse(self.chemenv.list_coords)
+ elif self.which_bonds == "cation-anion":
+ # make a new list
+ self.lse = Lse(self.chemenv.list_coords, self.chemenv.valences)
+
+
+
+[docs]
+ def get_information_all_bonds(self, summed_spins: bool = True):
+ """
+ Gather all information on the bonds within the compound with this method.
+
+ Returns:
+ None
+
+ """
+ if self.which_bonds == "cation-anion":
+ # this will only analyze cation anion bonds which simplifies the analysis
+ self.seq_ineq_ions = []
+ self.seq_coord_ions = []
+ self.seq_infos_bonds = []
+ self.seq_labels_cohps = []
+ self.seq_cohps = []
+ # only_bonds_to
+
+ self.anion_types = self.chemenv.anion_types
+ for ice, ce in enumerate(self.lse.coordination_environments):
+ # only look at inequivalent sites (use of symmetry to speed everything up!)!
+ # only look at those cations that have cation-anion bonds
+ if ice in self.seq_equivalent_sites and ce[0]["ce_symbol"] is not None:
+ self.seq_ineq_ions.append(ice)
+
+ self.seq_coord_ions.append(ce[0]["ce_symbol"])
+ self.seq_infos_bonds.append(self.chemenv.get_info_icohps_to_neighbors([ice]))
+
+ aniontype_labels = []
+ aniontype_cohps = []
+
+ # go through all anions in the structure!
+ for anion in self.anion_types:
+ # get labels and summed cohp objects
+ labels, summedcohps = self.chemenv.get_info_cohps_to_neighbors(
+ path_to_cohpcar=self.path_to_cohpcar,
+ obj_cohpcar=self.completecohp_obj,
+ isites=[ice],
+ summed_spin_channels=summed_spins,
+ per_bond=False,
+ only_bonds_to=[str(anion)],
+ )
+
+ aniontype_labels.append(labels)
+ aniontype_cohps.append(summedcohps)
+
+ self.seq_labels_cohps.append(aniontype_labels)
+ self.seq_cohps.append(aniontype_cohps)
+
+ elif self.which_bonds == "all":
+ # this will only analyze all bonds
+
+ self.seq_ineq_ions = []
+ self.seq_coord_ions = []
+ self.seq_infos_bonds = []
+ self.seq_labels_cohps = []
+ self.seq_cohps = []
+ # only_bonds_to
+ self.elements = self.structure.composition.elements
+ # self.anion_types = self.chemenv.get_anion_types()
+ for ice, ce in enumerate(self.lse.coordination_environments):
+ # only look at inequivalent sites (use of symmetry to speed everything up!)!
+ # only look at those cations that have cation-anion bonds
+ if ice in self.seq_equivalent_sites and ce[0]["ce_symbol"] is not None:
+ self.seq_ineq_ions.append(ice)
+ self.seq_coord_ions.append(ce[0]["ce_symbol"])
+ self.seq_infos_bonds.append(self.chemenv.get_info_icohps_to_neighbors([ice]))
+
+ type_labels = []
+ type_cohps = []
+
+ for element in self.elements:
+ # get labels and summed cohp objects
+ labels, summedcohps = self.chemenv.get_info_cohps_to_neighbors(
+ path_to_cohpcar=self.path_to_cohpcar,
+ obj_cohpcar=self.completecohp_obj,
+ isites=[ice],
+ onlycation_isites=False,
+ summed_spin_channels=summed_spins,
+ per_bond=False,
+ only_bonds_to=[str(element)],
+ )
+
+ type_labels.append(labels)
+ type_cohps.append(summedcohps)
+
+ self.seq_labels_cohps.append(type_labels)
+ self.seq_cohps.append(type_cohps)
+
+
+
+[docs]
+ def get_site_bond_resolved_labels(self):
+ """
+ Return relevant bond labels for each symmetrically independent site.
+
+ Returns:
+ dict with bond labels for each site, e.g.
+ {'Na1: Na-Cl': ['21', '23', '24', '27', '28', '30']}
+
+ """
+ bonds = [[] for _ in range(len(self.seq_infos_bonds))] # type: ignore
+ labels = [[] for _ in range(len(self.seq_infos_bonds))] # type: ignore
+ for inx, bond_info in enumerate(self.seq_infos_bonds):
+ for ixx, val in enumerate(bond_info.atoms):
+ label_srt = sorted(val.copy())
+ bonds[inx].append(
+ self.structure.sites[bond_info.central_isites[0]].species_string
+ + str(bond_info.central_isites[0] + 1)
+ + ": "
+ + label_srt[0].strip("0123456789")
+ + "-"
+ + label_srt[1].strip("0123456789")
+ )
+ labels[inx].append(bond_info.labels[ixx])
+
+ label_data = {}
+ for indx, atom_pairs in enumerate(bonds):
+ searched_atom_pairs = set(atom_pairs)
+ for search_item in searched_atom_pairs:
+ indices = [i for i, pair in enumerate(atom_pairs) if pair == search_item]
+ filtered_bond_label_list = [labels[indx][i] for i in indices]
+ label_data.update({search_item: filtered_bond_label_list})
+
+ return label_data
+
+
+ def _get_orbital_resolved_data(
+ self,
+ nameion: str,
+ iion: int,
+ labels: list[str],
+ bond_resolved_labels: dict[str, list[str]],
+ type_pop: str,
+ ):
+ """
+ Retrieve orbital-wise analysis data.
+
+ :param nameion: name of symmetrically relevant cation or anion
+ :param iion: index of symmetrically relevant cation or anion
+ :param labels: list of bond label names
+ :param bond_resolved_labels: dict of bond labels from ICOHPLIST resolved for each bond
+ :param type_pop: population type analyzed. e.g. COHP or COOP or COBI
+
+ Returns:
+ dict consisting of relevant orbitals (contribution > 5 % to overall ICOHP or ICOBI or ICOOP),
+ bonding and antibonding percentages with bond label names as keys.
+ """
+ orb_resolved_bond_info = {}
+ for label in labels:
+ if label is not None:
+ bond_resolved_label_key = nameion + str(iion + 1) + ":" + label.split("x")[-1]
+ bond_labels = bond_resolved_labels[bond_resolved_label_key]
+ orb_combinations = self._get_orb_combinations()
+ grouped_orb_pairs = self._group_orb_pairs(bond_label=bond_labels[0], orb_combinations=orb_combinations)
+ # available_orbitals = list(self.chemenv.completecohp.orb_res_cohp[bond_labels[0]].keys())
+ # initialize empty list to store orb paris for bonding,
+ # antibonding integrals and percentages
+ bndg_orb_pair_list = []
+ bndg_orb_integral_list = []
+ bndg_orb_perc_list = []
+ bndg_orb_icohp_list = []
+ antibndg_orb_integral_list = []
+ antibndg_orb_perc_list = []
+ antibndg_orb_pair_list = []
+ antibndg_orb_icohp_list = []
+
+ # get total summed cohps using label list
+ cohp_summed = self.chemenv.completecohp.get_summed_cohp_by_label_list(label_list=bond_labels)
+ if type_pop.lower() == "cohp":
+ (
+ antibndg_tot,
+ per_anti_tot,
+ bndg_tot,
+ per_bndg_tot,
+ ) = self._integrate_antbdstates_below_efermi(cohp=cohp_summed, start=self.start)
+ else:
+ (
+ bndg_tot,
+ per_bndg_tot,
+ antibndg_tot,
+ per_anti_tot,
+ ) = self._integrate_antbdstates_below_efermi(cohp=cohp_summed, start=self.start)
+
+ orb_bonding_dict_data = {} # type: ignore
+ # For each orbital collect the contributions of summed bonding
+ # and antibonding interactions separately
+ for orb in grouped_orb_pairs:
+ mapped_bond_labels = self._get_bond_orb_label_mapped_list(
+ bond_labels=bond_labels, orb_pair=grouped_orb_pairs, root_orb_pair=orb
+ )
+ # for orb in available_orbitals:
+ cohp_summed_orb = self.chemenv.completecohp.get_summed_cohp_by_label_and_orbital_list(
+ label_list=mapped_bond_labels, orbital_list=grouped_orb_pairs[orb] * len(bond_labels)
+ )
+
+ if type_pop.lower() == "cohp":
+ (
+ antibndg_orb,
+ per_anti_orb,
+ bndg_orb,
+ per_bndg_orb,
+ ) = self._integrate_antbdstates_below_efermi(cohp=cohp_summed_orb, start=self.start)
+ else:
+ (
+ bndg_orb,
+ per_bndg_orb,
+ antibndg_orb,
+ per_anti_orb,
+ ) = self._integrate_antbdstates_below_efermi(cohp=cohp_summed_orb, start=self.start)
+
+ # replace nan values with zero (tackle numerical integration issues)
+ bndg_orb = bndg_orb if not np.isnan(bndg_orb) else 0
+ per_bndg_orb = per_bndg_orb if not np.isnan(per_bndg_orb) else 0
+ bndg_tot = bndg_tot if not np.isnan(bndg_tot) else 0
+ per_bndg_tot = per_bndg_tot if not np.isnan(per_bndg_tot) else 0
+ # skip collecting orb contributions if no summed bonding contribution exists
+ if bndg_tot > 0:
+ orb_icohps_bndg = []
+ for bond_label in bond_labels:
+ for sub_orb in grouped_orb_pairs[orb]:
+ orb_icohp_bn = self.chemenv.Icohpcollection.get_icohp_by_label(
+ label=bond_label, orbitals=sub_orb
+ )
+ orb_icohps_bndg.append(orb_icohp_bn)
+ bndg_orb_pair_list.append(orb)
+ bndg_orb_icohp_list.append(orb_icohps_bndg)
+ bndg_orb_integral_list.append(bndg_orb)
+ bndg_orb_perc_list.append(per_bndg_orb)
+
+ # replace nan values with zero (tackle numerical integration issues)
+ antibndg_orb = antibndg_orb if not np.isnan(antibndg_orb) else 0
+ per_anti_orb = per_anti_orb if not np.isnan(per_anti_orb) else 0
+ antibndg_tot = antibndg_tot if not np.isnan(antibndg_tot) else 0
+ per_anti_tot = per_anti_tot if not np.isnan(per_anti_tot) else 0
+ # skip collecting orb contributions if no summed antibonding contribution exists
+ if antibndg_tot > 0:
+ orb_icohps_anti = []
+ for bond_label in bond_labels:
+ for sub_orb in grouped_orb_pairs[orb]:
+ orb_icohp_an = self.chemenv.Icohpcollection.get_icohp_by_label(
+ label=bond_label, orbitals=sub_orb
+ )
+ orb_icohps_anti.append(orb_icohp_an)
+
+ antibndg_orb_pair_list.append(orb)
+ antibndg_orb_icohp_list.append(orb_icohps_anti)
+ antibndg_orb_integral_list.append(antibndg_orb)
+ antibndg_orb_perc_list.append(per_anti_orb)
+
+ # Populate the dictionary with relevant orbitals for bonding interactions
+ for inx, bndg_orb_pair in enumerate(bndg_orb_pair_list):
+ bndg_contri_perc = round(bndg_orb_integral_list[inx] / sum(bndg_orb_integral_list), 2)
+ # filter out very small bonding interactions (<self.orbital_cutoff)
+ if bndg_contri_perc > self.orbital_cutoff:
+ if bndg_orb_pair in orb_bonding_dict_data:
+ orb_bonding_dict_data[bndg_orb_pair].update(
+ {
+ "orb_contribution_perc_bonding": bndg_contri_perc,
+ "bonding": {
+ "integral": bndg_orb_integral_list[inx],
+ "perc": bndg_orb_perc_list[inx],
+ },
+ }
+ )
+ else:
+ orb_bonding_dict_data[bndg_orb_pair] = {
+ f"I{type_pop}_mean": round(np.mean(bndg_orb_icohp_list[inx]), 4),
+ f"I{type_pop}_sum": round(np.sum(bndg_orb_icohp_list[inx]), 4),
+ "orb_contribution_perc_bonding": round(
+ bndg_orb_integral_list[inx] / sum(bndg_orb_integral_list),
+ 2,
+ ),
+ "bonding": {
+ "integral": bndg_orb_integral_list[inx],
+ "perc": bndg_orb_perc_list[inx],
+ },
+ "relevant_sub_orbitals": grouped_orb_pairs[bndg_orb_pair],
+ }
+
+ # Populate the dictionary with relevant orbitals for antibonding interactions
+ for inx, antibndg_orb_pair in enumerate(antibndg_orb_pair_list):
+ antibndg_contri_perc = round(
+ antibndg_orb_integral_list[inx] / sum(antibndg_orb_integral_list),
+ 2,
+ )
+ # filter out very small antibonding interactions (<self.orbital_cutoff)
+ if antibndg_contri_perc > self.orbital_cutoff:
+ if antibndg_orb_pair in orb_bonding_dict_data:
+ orb_bonding_dict_data[antibndg_orb_pair].update(
+ {
+ "orb_contribution_perc_antibonding": round(
+ antibndg_orb_integral_list[inx] / sum(antibndg_orb_integral_list),
+ 2,
+ ),
+ "antibonding": {
+ "integral": antibndg_orb_integral_list[inx],
+ "perc": antibndg_orb_perc_list[inx],
+ },
+ }
+ )
+ else:
+ orb_bonding_dict_data[antibndg_orb_pair] = {
+ f"I{type_pop}_mean": round(np.mean(antibndg_orb_icohp_list[inx]), 4),
+ f"I{type_pop}_sum": round(np.sum(antibndg_orb_icohp_list[inx]), 4),
+ "orb_contribution_perc_antibonding": round(
+ antibndg_orb_integral_list[inx] / sum(antibndg_orb_integral_list),
+ 2,
+ ),
+ "antibonding": {
+ "integral": antibndg_orb_integral_list[inx],
+ "perc": antibndg_orb_perc_list[inx],
+ },
+ "relevant_sub_orbitals": grouped_orb_pairs[antibndg_orb_pair],
+ }
+
+ orb_bonding_dict_data["relevant_bonds"] = bond_labels # type: ignore
+
+ orb_resolved_bond_info[bond_resolved_label_key] = orb_bonding_dict_data
+
+ return orb_resolved_bond_info
+
+ def _get_bond_resolved_data_stats(self, orb_resolved_bond_data: dict):
+ """
+ Retrieve the maximum bonding and anti-bonding orbital contributions.
+
+ :param orb_resolved_bond_data: A dictionary with orbital names as keys and corresponding bonding data
+
+ Returns:
+ dict with orbital data stats the site for relevant orbitals, e.g.
+ {'orbital_summary_stats': {'max_bonding_contribution': {'3p-3p': 0.41},
+ 'max_antibonding_contribution': {'3s-3p': 0.39}}}
+
+ """
+ # get max orbital bonding and contribution for the site
+ orb_pairs_bndg = []
+ orb_pairs_antibndg = []
+ orb_contri_bndg = []
+ orb_contri_antibndg = []
+ orbital_summary_stats = {"orbital_summary_stats": {}} # type: ignore
+ if orb_resolved_bond_data:
+ for orb_pair, data in orb_resolved_bond_data.items():
+ if "orb_contribution_perc_bonding" in data:
+ orb_pairs_bndg.append(orb_pair)
+ orb_contri_bndg.append(data["orb_contribution_perc_bonding"])
+
+ if "orb_contribution_perc_antibonding" in data:
+ orb_pairs_antibndg.append(orb_pair)
+ orb_contri_antibndg.append(data["orb_contribution_perc_antibonding"])
+
+ if orb_contri_bndg:
+ max_orb_contri_bndg = max(orb_contri_bndg)
+ max_orb_contri_bndg_inxs = [
+ inx for inx, orb_contri in enumerate(orb_contri_bndg) if orb_contri == max_orb_contri_bndg
+ ]
+ max_orb_contri_bndg_dict = {}
+ for inx in max_orb_contri_bndg_inxs:
+ max_orb_contri_bndg_dict[orb_pairs_bndg[inx]] = orb_contri_bndg[inx]
+ orbital_summary_stats["orbital_summary_stats"]["max_bonding_contribution"] = max_orb_contri_bndg_dict
+ if orb_contri_antibndg:
+ max_orb_contri_antibndg = max(orb_contri_antibndg)
+ max_antibndg_contri_inxs = [
+ inx
+ for inx, orb_anti_per in enumerate(orb_contri_antibndg)
+ if orb_anti_per == max_orb_contri_antibndg
+ ]
+ max_antibndg_contri_dict = {}
+ for inx in max_antibndg_contri_inxs:
+ max_antibndg_contri_dict[orb_pairs_antibndg[inx]] = orb_contri_antibndg[inx]
+ orbital_summary_stats["orbital_summary_stats"]["max_antibonding_contribution"] = (
+ max_antibndg_contri_dict
+ )
+
+ return orbital_summary_stats
+
+
+[docs]
+ def get_site_orbital_resolved_labels(self):
+ """
+ Return relevant orbitals and bond labels for each symmetrically independent site.
+
+ Returns:
+ dict with bond labels for each site for relevant orbitals, e.g.
+ {'Na1: Na-Cl': {'3p-3s': {'bond_labels': ['21', '23', '24', '27', '28', '30'],
+ 'relevant_sub_orbitals': ['3py-3s', '3pz-3s', '3px-3s']}}
+ """
+ site_bond_labels = self.get_site_bond_resolved_labels()
+ orb_plot_data = {atom_pair: {} for atom_pair in site_bond_labels}
+ if self.orbital_resolved:
+ for site_index, cba_data in self.condensed_bonding_analysis["sites"].items():
+ for atom in cba_data["bonds"]:
+ for orb_pair in cba_data["bonds"][atom]["orbital_data"]:
+ if orb_pair not in ("orbital_summary_stats", "relevant_bonds"):
+ atom_pair = [cba_data["ion"], atom]
+ atom_pair.sort()
+ key = (
+ self.structure.sites[site_index].species_string
+ + str(site_index + 1)
+ + ": "
+ + "-".join(atom_pair)
+ )
+ label_list = site_bond_labels[key]
+ relevant_sub_orbitals = cba_data["bonds"][atom]["orbital_data"][orb_pair][
+ "relevant_sub_orbitals"
+ ]
+ orb_plot_data[key].update(
+ {orb_pair: {"bond_labels": label_list, "relevant_sub_orbitals": relevant_sub_orbitals}}
+ )
+ else:
+ print("Please set orbital_resolved to True when instantiating Analysis object, to get this data")
+
+ return orb_plot_data
+
+
+ @staticmethod
+ def _get_strenghts_for_each_bond(pairs: list[list[str]], strengths: list[float], nameion: str | None = None):
+ """
+ Return a dictionary of bond strengths.
+
+ :param pairs: list of list including labels for the atoms, e.g., [['O3', 'Cu1'], ['O3', 'Cu1']]
+ :param strengths: list that gives the icohp strengths as a float, [-1.86287, -1.86288]
+ :param nameion: string including the name of the cation in the list, e.g Cu1
+
+ Returns:
+ dict including inormation on icohps for each bond type, e.g.
+ {'Yb-Sb': [-1.59769, -2.14723, -1.7925, -1.60773, -1.80149, -2.14335]}
+
+
+ """
+ dict_strenghts = {} # type: ignore
+
+ for pair, strength in zip(pairs, strengths):
+ if nameion is not None:
+ new = [
+ LobsterNeighbors._split_string(pair[0])[0],
+ LobsterNeighbors._split_string(pair[1])[0],
+ ]
+ new = Analysis._sort_name(new, nameion)
+ string_here = new[0] + "-" + new[1]
+ else:
+ new = sorted(
+ [
+ LobsterNeighbors._split_string(pair[0])[0],
+ LobsterNeighbors._split_string(pair[1])[0],
+ ]
+ )
+ string_here = new[0] + "-" + new[1]
+
+ if string_here not in dict_strenghts:
+ dict_strenghts[string_here] = []
+ dict_strenghts[string_here].append(strength)
+ return dict_strenghts
+
+ @staticmethod
+ def _sort_name(pair: list[str], nameion: str | None = None):
+ """
+ Place the cation first in a list of name strings.
+
+ :param pair: ["O","Cu"]
+ :param nameion: "Cu"
+
+ Returns:
+ will return list of str, e.g. ["Cu", "O"]
+
+ """
+ if nameion is not None:
+ new = []
+ if pair[0] == nameion:
+ new.append(pair[0])
+ new.append(pair[1])
+
+ elif pair[1] == nameion:
+ new.append(pair[1])
+ new.append(pair[0])
+
+ return new
+
+ @staticmethod
+ def _sort_orbital_atom_pair(
+ atom_pair: list[str],
+ label: str,
+ complete_cohp: CompleteCohp,
+ orb_pair: str,
+ ):
+ """
+ Place the cation first in a list of name strings and add the associated orbital name alongside the atom name.
+
+ :param atom_pair: list of atom pair with cation first eg., ["Cl","Na"]
+ :param label: LOBSTER relevant bond label eg ., "3"
+ :param complete_cohp: pymatgen CompleteCohp object
+ :param orb_pair: relevant orbital pair eg., "2p-3s"
+
+ Returns:
+ will return list of str, e.g. ["Na(2p)", "Cl(3s)"]
+
+ """
+ orb_atom = {} # type: ignore
+ orb_pair_list = orb_pair.split("-")
+ # get orbital associated to the atom and store in a dict
+ for _inx, (site, site_orb) in enumerate(zip(complete_cohp.bonds[label]["sites"], orb_pair_list)):
+ if site.species_string in orb_atom: # check necessary for bonds between same atoms
+ orb_atom[site.species_string].append(site_orb)
+ else:
+ orb_atom[site.species_string] = [site_orb]
+
+ orb_atom_list = []
+ # add orbital name next to atom_pair
+ for inx, atom in enumerate(atom_pair):
+ # check to ensure getting 2nd orbital if bond is between same atomic species
+ if inx == 1 and len(orb_atom.get(atom)) > 1: # type: ignore
+ atom_with_orb_name = f"{atom}({orb_atom.get(atom)[1]})" # type: ignore
+ else:
+ atom_with_orb_name = f"{atom}({orb_atom.get(atom)[0]})" # type: ignore
+ orb_atom_list.append(atom_with_orb_name)
+
+ return orb_atom_list
+
+ def _get_antibdg_states(self, cohps, labels: list[str], nameion: str | None = None, limit=0.01):
+ """
+ Return a dictionary containing information on anti-bonding states.
+
+ e.g., similar to: {'Cu-O': True, 'Cu-F': True}
+
+ :param cohps: list of pymatgen.electronic_structure.cohp.Cohp objects
+ :param labels: ['2 x Cu-O', '4 x Cu-F']
+ :param nameion: string of the cation name, e.g. "Cu"
+ :param limit: limit to detect antibonding states
+
+ Returns:
+ dict including in formation on whether antibonding interactions exist,
+ e.g., {'Cu-O': True, 'Cu-F': True}
+
+
+ """
+ dict_antibd = {}
+ for label, cohp in zip(labels, cohps):
+ if label is not None:
+ if nameion is not None:
+ new = label.split(" ")[2].split("-")
+ sorted_new = self._sort_name(new, nameion)
+ new_label = sorted_new[0] + "-" + sorted_new[1]
+ else:
+ new = label.split(" ")[2].split("-")
+ sorted_new = sorted(new.copy())
+ new_label = sorted_new[0] + "-" + sorted_new[1]
+
+ antbd = cohp.has_antibnd_states_below_efermi(limit=limit)
+ if Spin.down in antbd:
+ dict_antibd[new_label] = antbd[Spin.up] or antbd[Spin.down]
+ else:
+ dict_antibd[new_label] = antbd[Spin.up]
+
+ return dict_antibd
+
+ def _integrate_antbdstates_below_efermi_for_set_cohps(self, labels: list[str], cohps, nameion: str):
+ """
+ Return a dictionary containing information on antibonding states.
+
+ .. warning:: NEEDS MORE TESTS
+
+ It is important to note that only the energy range that has been computed can be considered
+ (i.e., this might not be all)
+
+ e.g. output: {'Cu-O': {'integral': 4.24374775705, 'perc': 5.7437713186999995},
+ 'Cu-F': {'integral': 3.07098300965, 'perc': 4.25800841445}}
+
+ :param cohps: list of pymatgen.electronic_structure.cohp.Cohp objects
+ :param labels: ['2 x Cu-O', '4 x Cu-F']
+ :param nameion: string of the cation name, e.g. "Cu"
+
+ Returns:
+ dict including in formation on whether antibonding interactions exist,
+ e.g., {'Cu-O': {'integral': 4.24374775705, 'perc': 5.7437713186999995},
+ 'Cu-F': {'integral': 3.07098300965, 'perc': 4.25800841445}}}
+ """
+ dict_bd_antibd = {}
+ for label, cohp in zip(labels, cohps):
+ if label is not None:
+ new = label.split(" ")[2].split("-")
+ sorted_new = self._sort_name(new, nameion)
+ new_label = sorted_new[0] + "-" + sorted_new[1]
+ if not self.are_cobis and not self.are_coops:
+ (
+ integral,
+ perc,
+ integral2,
+ perc2,
+ ) = self._integrate_antbdstates_below_efermi(cohp, start=self.start)
+ else:
+ (
+ integral2,
+ perc2,
+ integral,
+ perc,
+ ) = self._integrate_antbdstates_below_efermi(cohp, start=self.start)
+
+ if integral == 0 and integral2 != 0.0:
+ dict_bd_antibd[new_label] = {
+ "bonding": {"integral": integral2, "perc": perc2},
+ "antibonding": {"integral": integral, "perc": 0.0},
+ }
+ elif integral2 == 0.0 and integral != 0.0:
+ dict_bd_antibd[new_label] = {
+ "bonding": {"integral": integral2, "perc": 0.0},
+ "antibonding": {"integral": integral, "perc": perc},
+ }
+ elif integral == 0.0 and integral2 == 0.0:
+ dict_bd_antibd[new_label] = {
+ "bonding": {"integral": integral2, "perc": 0.0},
+ "antibonding": {"integral": integral, "perc": 0.0},
+ }
+ else:
+ dict_bd_antibd[new_label] = {
+ "bonding": {"integral": integral2, "perc": perc2},
+ "antibonding": {"integral": integral, "perc": perc},
+ }
+
+ return dict_bd_antibd
+
+ def _integrate_antbdstates_below_efermi(self, cohp, start: float | None):
+ """
+ Integrate the cohp data to compute bonding and anti-bonding contribution below efermi.
+
+ .. warning:: NEEDS MORE TESTS
+
+ This integrates the whole COHP curve that has been computed.
+ The energy range is very important.
+ At present the energy range considered is dependent on COHPstartEnergy
+ set during lobster runs. The bonding / antibonding integral values are sensitive to this parameter.
+ If COHPstartEnergy value does not cover entire range of VASP calculations then
+ absolute value of ICOHP_sum might not be equivalent to (bonding- antibonding) integral values.
+
+ :param cohp: cohp object
+ :param start: integration start energy in eV , eg start = -15
+
+ Returns:
+ absolute value of antibonding, percentage value of antibonding,
+ absolute value of bonding and percentage value of bonding interactions
+ """
+ warnings.warn(
+ "The bonding, antibonding integral/percent values are numerical estimate."
+ " These values are sensitive to COHPstartEnergy parameter."
+ " If COHPstartEnergy value does not cover entire range of VASP calculations then"
+ " absolute value of ICOHP_sum might not be equivalent to (bonding- antibonding) integral values.",
+ stacklevel=2,
+ )
+
+ from scipy.integrate import trapezoid
+
+ def integrate_positive(y, x):
+ """
+ Integrate only bonding interactions of COHPs.
+
+ :param y: COHP values
+ :param x: Energy values
+
+ Returns:
+ integrated value of bonding interactions
+ """
+ y = np.asanyarray(y)
+ x = np.asanyarray(x)
+
+ bonding = trapezoid(y, x)
+
+ return np.round(bonding, 2)
+
+ def integrate_negative(y, x):
+ """
+ Integrate only anti-bonding interactions of COHPs.
+
+ :param y: COHP values
+ :param x: Energy values
+
+ Returns:
+ integrated value of anti-bonding interactions
+ """
+ y = np.asanyarray(y)
+ x = np.asanyarray(x)
+ antibonding = trapezoid(y, x)
+
+ return np.round(antibonding, 2)
+
+ # will integrate spin.up and spin.down only below efermi
+ energies_corrected = cohp.energies - cohp.efermi
+ summedcohp = cohp.cohp[Spin.up] + cohp.cohp[Spin.down] if Spin.down in cohp.cohp else cohp.cohp[Spin.up]
+
+ cohp_bf = []
+ en_bf = []
+
+ for i, en in enumerate(energies_corrected):
+ if (start is None) and en <= 0:
+ en_bf.append(en)
+ cohp_bf.append(-1 * summedcohp[i])
+ if (start is not None) and 0 >= en >= start:
+ en_bf.append(en)
+ cohp_bf.append(-1 * summedcohp[i])
+
+ # Separate the bonding and antibonding COHP values in separate lists
+ pos = []
+ en_pos = []
+ neg = []
+ en_neg = []
+
+ for i, scohp in enumerate(cohp_bf):
+ if scohp >= 0:
+ pos.append(scohp)
+ en_pos.append(energies_corrected[i])
+
+ for i, scohp in enumerate(cohp_bf):
+ if scohp <= 0:
+ neg.append(-1 * scohp)
+ en_neg.append(energies_corrected[i])
+
+ antibonding = integrate_negative(y=neg, x=en_neg)
+
+ bonding = integrate_positive(y=pos, x=en_pos)
+
+ return (
+ antibonding,
+ np.round(abs(antibonding) / (abs(bonding) + abs(antibonding)), 5),
+ bonding,
+ np.round(abs(bonding) / (abs(bonding) + abs(antibonding)), 5),
+ )
+
+ def _get_pop_type(self):
+ """
+ Return the type of the input population file.
+
+ Returns:
+ A String of analysed population can be COOP/COBI/COHP
+ """
+ if self.are_cobis:
+ type_pop = "COBI"
+ elif self.are_coops:
+ type_pop = "COOP"
+ else:
+ type_pop = "COHP"
+
+ return type_pop
+
+ @staticmethod
+ def _get_bond_dict(
+ bond_strength_dict: dict,
+ small_antbd_dict: dict,
+ nameion: str | None = None,
+ large_antbd_dict: dict | None = None,
+ type_pop: str | None = None,
+ ):
+ """
+ Return a bond_dict that contains information for each site.
+
+ :param bond_strength_dict: dict with bond names as key and lists of bond strengths as items
+ :param small_antbd_dict: dict including if there are antibonding interactions, {'Yb-Sb': False}
+ :param nameion: name of the cation, e.g. Yb
+ :param large_antbd_dict: will be implemented later
+ :param type_pop: population type analyzed. eg. COHP
+
+ Returns:
+ Eg., if type_pop == 'COHP', will return
+ dict including information on the anion (as label) and the ICOHPs in the item of the dict
+ ICOHP_mean refers to the mean ICOHP in eV
+ ICOHP_sum refers to the sum of the ICOHPs in eV
+ has_antibdg_states_below_Efermi is True if there are antibonding interactions below Efermi
+ "number_of_bonds" will count the numbers of bonds to the cation
+
+ Example:
+ {'Sb': {'ICOHP_mean': '-1.85', 'ICOHP_sum': '-11.09',
+ 'has_antibdg_states_below_Efermi': False, 'number_of_bonds': 6}}
+
+ """
+ bond_dict = {}
+
+ for key, item in bond_strength_dict.items():
+ if nameion is not None:
+ a = key.split("-")[0]
+ b = key.split("-")[1]
+ if a == nameion:
+ key_here = b
+ elif b == nameion:
+ key_here = a
+
+ if large_antbd_dict is None:
+ bond_dict[key_here] = {
+ f"I{type_pop}_mean": str(round(np.mean(item), 2)),
+ f"I{type_pop}_sum": str(round(np.sum(item), 2)),
+ "has_antibdg_states_below_Efermi": small_antbd_dict[key],
+ "number_of_bonds": len(item),
+ }
+ else:
+ bond_dict[key_here] = {
+ f"I{type_pop}_mean": str(round(np.mean(item), 2)),
+ f"I{type_pop}_sum": str(round(np.sum(item), 2)),
+ "has_antibdg_states_below_Efermi": small_antbd_dict[key],
+ "number_of_bonds": len(item),
+ "perc_antibdg_states_below_Efermi": large_antbd_dict[key],
+ }
+
+ return bond_dict
+
+ @staticmethod
+ def _get_orb_combinations():
+ """
+ Get a list of unique 2-length permutations and combinations.
+
+ Generates 2-length permutations and combinations with replacement from the set ['s', 'p', 'd', 'f']
+
+ Returns:
+ list[tuple[str, str]]: A list of tuples, each containing two elements.
+ """
+ list_orbs_comb: list[tuple[str, str]] = [] # type: ignore
+
+ # Add all 2-length permutations
+ for perm in permutations(["s", "p", "d", "f"], 2):
+ list_orbs_comb.append(perm) # noqa : PERF402
+
+ # Add 2-length combinations with replacement, ensuring no duplicates
+ for comb in combinations_with_replacement(["s", "p", "d", "f"], 2):
+ if comb not in list_orbs_comb:
+ list_orbs_comb.append(comb)
+
+ return list_orbs_comb
+
+ def _group_orb_pairs(self, bond_label: str, orb_combinations: list[tuple[str, str]]) -> dict[str, list[str]]:
+ """
+ Group orbital pairs based on the provided bond label.
+
+ :param bond_label: The bond label to filter the orbitals.
+ :param orb_combinations: A list of tuples containing orbital combinations.
+
+ Returns:
+ dict[str, List[str]]: A dictionary where the keys are top level orbital pairs and the values are lists of
+ sub orbitals associated to the bond label.
+ """
+ orb_pair = {} # type: ignore
+ bond_label_int = int(bond_label) - 1 # convert label to int to access orbital data from icohpcollection
+ for sub_orb, data in self.chemenv.Icohpcollection._list_orb_icohp[bond_label_int].items():
+ for orb1, orb2 in orb_combinations:
+ if orb1 in data["orbitals"][0][1].name and orb2 in data["orbitals"][1][1].name:
+ root_orb_pair = f"{data['orbitals'][0][0]}{orb1}-{data['orbitals'][1][0]}{orb2}"
+ if root_orb_pair not in orb_pair:
+ orb_pair[root_orb_pair] = []
+ orb_pair[root_orb_pair].append(sub_orb)
+ return orb_pair
+
+ @staticmethod
+ def _get_bond_orb_label_mapped_list(orb_pair: dict[str, list[str]], bond_labels: list[str], root_orb_pair: str):
+ """
+ Get a lists of bond labels mapped to the corresponding orbital pair.
+
+ :param orb_pair: A dictionary containing orbital pairs as keys and lists of
+ sub orbitals as values.
+ :param bond_labels: A list of bond labels.
+ :param root_orb_pair: The root key in orb_pair use to map bond labels list.
+
+ Returns:
+ list: A list where the items of bond_labels are repeated based on the
+ length of orb_pair[root_orb_pair].
+ """
+ return [item for item in bond_labels for _ in range(len(orb_pair[root_orb_pair]))]
+
+
+[docs]
+ def set_condensed_bonding_analysis(self):
+ """
+ Condense the bonding analysis into a summary dictionary.
+
+ Returns:
+ None
+
+ """
+ self.condensed_bonding_analysis = {}
+ # which icohps are considered
+ if self.which_bonds == "cation-anion":
+ limit_icohps = self.chemenv._get_limit_from_extremum(
+ self.chemenv.Icohpcollection,
+ self.cutoff_icohp,
+ adapt_extremum_to_add_cond=True,
+ additional_condition=1,
+ )
+ elif self.which_bonds == "all":
+ limit_icohps = self.chemenv._get_limit_from_extremum(
+ self.chemenv.Icohpcollection,
+ self.cutoff_icohp,
+ adapt_extremum_to_add_cond=True,
+ additional_condition=0,
+ )
+ # formula of the compound
+ formula = str(self.structure.composition.reduced_formula)
+ # set population type
+ type_pop = self._get_pop_type()
+ # how many inequivalent cations are in the structure
+ if self.which_bonds == "cation-anion":
+ number_considered_ions = len(self.seq_ineq_ions)
+ elif self.which_bonds == "all":
+ number_considered_ions = len(self.seq_ineq_ions)
+
+ # what was the maximum bond lengths that was considered
+ max_bond_lengths = max(self.chemenv.Icohpcollection._list_length)
+
+ # what are the charges for the cations in the structure
+ charge_list = self.chemenv.valences
+
+ # dictionary including bonding information for each site
+ site_dict = {}
+ if self.which_bonds == "cation-anion":
+ for ication, ce, cation_anion_infos, labels, cohps in zip(
+ self.seq_ineq_ions,
+ self.seq_coord_ions,
+ self.seq_infos_bonds,
+ self.seq_labels_cohps,
+ self.seq_cohps,
+ ):
+ namecation = str(self.structure[ication].specie)
+
+ # This will compute the mean strengths of ICOHPs
+ mean_icohps = self._get_strenghts_for_each_bond(
+ pairs=cation_anion_infos[4],
+ strengths=cation_anion_infos[1],
+ nameion=namecation,
+ )
+ # pairs, strengths, nameion
+ # will collect if there are antibonding states present
+ antbdg = self._get_antibdg_states(cohps, labels, namecation)
+ dict_antibonding = self._integrate_antbdstates_below_efermi_for_set_cohps(
+ labels, cohps, nameion=namecation
+ )
+ bond_dict = self._get_bond_dict(mean_icohps, antbdg, namecation, type_pop=type_pop)
+ bond_resolved_labels = self.get_site_bond_resolved_labels()
+
+ for cation_name, icohp_data in bond_dict.items():
+ for atom_pair, bonding_data in dict_antibonding.items():
+ if namecation == atom_pair.split("-")[0] and cation_name == atom_pair.split("-")[1]:
+ icohp_data["bonding"] = bonding_data["bonding"]
+ icohp_data["antibonding"] = bonding_data["antibonding"]
+ if self.orbital_resolved:
+ # get orb resolved data to be added
+ orb_resolved_bond_info = self._get_orbital_resolved_data(
+ nameion=namecation,
+ iion=ication,
+ labels=labels,
+ bond_resolved_labels=bond_resolved_labels,
+ type_pop=type_pop,
+ )
+ # match the dict key in bond_dict and get corresponding orbital data
+ for ion_atom_pair_orb in orb_resolved_bond_info:
+ orb_data_atom_pair = ion_atom_pair_orb.split(": ")[-1]
+ atom_pair_here = atom_pair.split("-")
+ atom_pair_here.sort()
+ if (
+ orb_data_atom_pair == "-".join(atom_pair_here)
+ and (namecation + str(ication + 1) + ":") in ion_atom_pair_orb
+ ):
+ icohp_data["orbital_data"] = orb_resolved_bond_info[ion_atom_pair_orb]
+
+ orb_data_stats = self._get_bond_resolved_data_stats(
+ orb_resolved_bond_data=orb_resolved_bond_info[ion_atom_pair_orb],
+ )
+
+ icohp_data["orbital_data"].update(orb_data_stats)
+
+ site_dict[ication] = {
+ "env": ce,
+ "bonds": bond_dict,
+ "ion": namecation,
+ "charge": charge_list[ication],
+ "relevant_bonds": cation_anion_infos[3],
+ }
+ elif self.which_bonds == "all":
+ for iion, ce, bond_infos, labels, cohps in zip(
+ self.seq_ineq_ions,
+ self.seq_coord_ions,
+ self.seq_infos_bonds,
+ self.seq_labels_cohps,
+ self.seq_cohps,
+ ):
+ nameion = str(self.structure[iion].specie)
+
+ # This will compute the mean strengths of ICOHPs
+ mean_icohps = self._get_strenghts_for_each_bond(
+ pairs=bond_infos[4], strengths=bond_infos[1], nameion=None
+ )
+ # pairs, strengths, nameion
+ # will collect if there are antibonding states present
+ antbdg = self._get_antibdg_states(cohps, labels, nameion=None)
+
+ dict_antibonding = self._integrate_antbdstates_below_efermi_for_set_cohps(labels, cohps, nameion)
+
+ bond_dict = self._get_bond_dict(mean_icohps, antbdg, nameion=nameion, type_pop=type_pop)
+ bond_resolved_labels = self.get_site_bond_resolved_labels()
+
+ for cation_name, icohp_data in bond_dict.items():
+ for atom_pair, bonding_data in dict_antibonding.items():
+ if nameion == atom_pair.split("-")[0] and cation_name == atom_pair.split("-")[1]:
+ icohp_data["bonding"] = bonding_data["bonding"]
+ icohp_data["antibonding"] = bonding_data["antibonding"]
+ if self.orbital_resolved:
+ # get orb resolved data to be added
+ orb_resolved_bond_info = self._get_orbital_resolved_data(
+ nameion=nameion,
+ iion=iion,
+ labels=labels,
+ bond_resolved_labels=bond_resolved_labels,
+ type_pop=type_pop,
+ )
+ # match the dict key in bond_dict and get corresponding orbital data
+ for ion_atom_pair_orb in orb_resolved_bond_info:
+ orb_data_atom_pair = ion_atom_pair_orb.split(": ")[-1]
+ atom_pair_here = atom_pair.split("-")
+ atom_pair_here.sort()
+ if (
+ orb_data_atom_pair == "-".join(atom_pair_here)
+ and (nameion + str(iion + 1) + ":") in ion_atom_pair_orb
+ ):
+ icohp_data["orbital_data"] = orb_resolved_bond_info[ion_atom_pair_orb]
+
+ orb_data_stats = self._get_bond_resolved_data_stats(
+ orb_resolved_bond_data=orb_resolved_bond_info[ion_atom_pair_orb],
+ )
+
+ icohp_data["orbital_data"].update(orb_data_stats)
+
+ site_dict[iion] = {
+ "env": ce,
+ "bonds": bond_dict,
+ "ion": nameion,
+ "charge": charge_list[iion],
+ "relevant_bonds": bond_infos[3],
+ }
+
+ if self.path_to_madelung is None and self.madelung_obj is None:
+ if self.which_bonds == "cation-anion":
+ # This sets the dictionary including the most important information on the compound
+ self.condensed_bonding_analysis = {
+ "formula": formula,
+ "max_considered_bond_length": max_bond_lengths,
+ f"limit_i{type_pop.lower()}": limit_icohps,
+ "number_of_considered_ions": number_considered_ions,
+ "sites": site_dict,
+ "type_charges": self.type_charge,
+ }
+ elif self.which_bonds == "all":
+ self.condensed_bonding_analysis = {
+ "formula": formula,
+ "max_considered_bond_length": max_bond_lengths,
+ f"limit_i{type_pop.lower()}": limit_icohps,
+ "number_of_considered_ions": number_considered_ions,
+ "sites": site_dict,
+ "type_charges": self.type_charge,
+ }
+ else:
+ madelung = MadelungEnergies(self.path_to_madelung) if self.path_to_madelung else self.madelung_obj
+ if self.type_charge == "Mulliken":
+ madelung_energy = madelung.madelungenergies_mulliken
+ elif self.type_charge == "Löwdin":
+ madelung_energy = madelung.madelungenergies_loewdin
+ else:
+ madelung_energy = None
+ # This sets the dictionary including the most important information on the compound
+ if self.which_bonds == "cation-anion":
+ self.condensed_bonding_analysis = {
+ "formula": formula,
+ "max_considered_bond_length": max_bond_lengths,
+ f"limit_i{type_pop.lower()}": limit_icohps,
+ "number_of_considered_ions": number_considered_ions,
+ "sites": site_dict,
+ "type_charges": self.type_charge,
+ "madelung_energy": madelung_energy,
+ }
+ elif self.which_bonds == "all":
+ self.condensed_bonding_analysis = {
+ "formula": formula,
+ "max_considered_bond_length": max_bond_lengths,
+ f"limit_i{type_pop.lower()}": limit_icohps,
+ "number_of_considered_ions": number_considered_ions,
+ "sites": site_dict,
+ "type_charges": self.type_charge,
+ "madelung_energy": madelung_energy,
+ }
+
+
+
+[docs]
+ def set_summary_dicts(self):
+ """
+ Set summary dict that can be used for correlations.
+
+ bond_dict that includes information on each bond
+
+ "has_antbd" tells if there are antbonding states
+ "ICOHP_mean" shows the mean of all ICOHPs in EV
+
+ {'Yb-Sb': { 'has_antbdg': False, 'ICOHP_mean': -1.7448},
+ 'Mn-Sb': { 'has_antbdg': True, 'ICOHP_mean': -1.525}}
+
+ a cation dict that includes all different coordination environments and counts for them
+ {'Na': {'T:4': 4, 'A:2': 4}, 'Si': {'T:6': 4, 'PP:6': 4}}
+
+ Returns:
+ None
+
+ """
+ relevant_ion_ids = [isite for isite in self.list_equivalent_sites if isite in self.seq_ineq_ions]
+ # set population type
+ type_pop = self._get_pop_type()
+
+ final_dict_bonds = {}
+ for key in relevant_ion_ids:
+ item = self.condensed_bonding_analysis["sites"][key]
+ for type, properties in item["bonds"].items():
+ label_list = [item["ion"], str(type)]
+ new_label = sorted(label_list.copy())
+ label = str(new_label[0]) + "-" + str(new_label[1])
+
+ if label not in final_dict_bonds:
+ final_dict_bonds[label] = {
+ "number_of_bonds": int(properties["number_of_bonds"]),
+ f"I{type_pop}_sum": float(properties[f"I{type_pop}_sum"]),
+ "has_antbdg": properties["has_antibdg_states_below_Efermi"],
+ }
+ else:
+ final_dict_bonds[label]["number_of_bonds"] += int(properties["number_of_bonds"])
+ final_dict_bonds[label][f"I{type_pop}_sum"] += float(properties[f"I{type_pop}_sum"])
+ final_dict_bonds[label]["has_antbdg"] = (
+ final_dict_bonds[label]["has_antbdg"] or properties["has_antibdg_states_below_Efermi"]
+ )
+ self.final_dict_bonds = {}
+ for key, item in final_dict_bonds.items():
+ self.final_dict_bonds[key] = {}
+ self.final_dict_bonds[key][f"I{type_pop}_mean"] = item[f"I{type_pop}_sum"] / (item["number_of_bonds"])
+ self.final_dict_bonds[key]["has_antbdg"] = item["has_antbdg"]
+
+ # rework, add all environments!
+ final_dict_ions = {}
+ for key in relevant_ion_ids:
+ if self.condensed_bonding_analysis["sites"][key]["ion"] not in final_dict_ions:
+ final_dict_ions[self.condensed_bonding_analysis["sites"][key]["ion"]] = [
+ self.condensed_bonding_analysis["sites"][key]["env"]
+ ]
+ else:
+ final_dict_ions[self.condensed_bonding_analysis["sites"][key]["ion"]].append(
+ self.condensed_bonding_analysis["sites"][key]["env"]
+ )
+
+ self.final_dict_ions = {}
+ for key, item in final_dict_ions.items():
+ self.final_dict_ions[key] = dict(Counter(item))
+
+
+
+[docs]
+ @staticmethod
+ def get_lobster_calc_quality_summary(
+ path_to_poscar: str | None = None,
+ path_to_lobsterout: str | None = None,
+ path_to_lobsterin: str | None = None,
+ path_to_potcar: str | None = None,
+ potcar_symbols: list | None = None,
+ path_to_charge: str | None = None,
+ path_to_bandoverlaps: str | None = None,
+ path_to_doscar: str | None = None,
+ path_to_vasprun: str | None = None,
+ structure_obj: Structure | None = None,
+ lobsterin_obj: Lobsterin | None = None,
+ lobsterout_obj: Lobsterout | None = None,
+ charge_obj: Charge | None = None,
+ bandoverlaps_obj: Bandoverlaps | None = None,
+ lobster_completedos_obj: LobsterCompleteDos | None = None,
+ vasprun_obj: Vasprun | None = None,
+ dos_comparison: bool = False,
+ e_range: list = [-5, 0],
+ n_bins: int | None = None,
+ bva_comp: bool = False,
+ ) -> dict:
+ """
+ Analyze LOBSTER calculation quality.
+
+ :param path_to_poscar: path to structure file
+ :param path_to_lobsterout: path to lobsterout file
+ :param path_to_lobsterin: path to lobsterin file
+ :param path_to_potcar: path to VASP potcar file
+ :param potcar_symbols: list of potcar symbols from potcar file (can be used if no potcar available)
+ :param path_to_charge: path to CHARGE.lobster file
+ :param path_to_bandoverlaps: path to bandOverlaps.lobster file
+ :param path_to_doscar: path to DOSCAR.lobster or DOSCAR.LSO.lobster file
+ :param path_to_vasprun: path to vasprun.xml file
+ :param structure_obj: pymatgen pymatgen.core.structure.Structure object
+ :param lobsterin_obj: pymatgen.lobster.io.Lobsterin object
+ :param lobsterout_obj: pymatgen lobster.io.Lobsterout object
+ :param charge_obj: pymatgen lobster.io.Charge object
+ :param bandoverlaps_obj: pymatgen lobster.io.BandOverlaps object
+ :param lobster_completedos_obj: pymatgen.electronic_structure.dos.LobsterCompleteDos object
+ :param vasprun_obj: pymatgen vasp.io.Vasprun object
+ :param dos_comparison: will compare DOS from VASP and LOBSTER and return tanimoto index
+ :param e_range: energy range for DOS comparisons
+ :param n_bins: number of bins to discretize DOS for comparisons
+ :param bva_comp: Compares LOBSTER charge signs with Bond valence charge signs
+
+ Returns:
+ A dict of summary of LOBSTER calculation quality by analyzing basis set used,
+ charge spilling from lobsterout/ PDOS comparisons of VASP and LOBSTER /
+ BVA charge comparisons
+
+ """
+ quality_dict = {}
+
+ if path_to_potcar and not potcar_symbols and not path_to_vasprun and not vasprun_obj:
+ potcar_names = Lobsterin._get_potcar_symbols(POTCAR_input=path_to_potcar)
+ elif not path_to_potcar and not path_to_vasprun and not vasprun_obj and potcar_symbols:
+ potcar_names = potcar_symbols
+ elif path_to_vasprun and not vasprun_obj:
+ vasprun = Vasprun(path_to_vasprun, parse_potcar_file=False, parse_eigen=False)
+ potcar_names = [potcar.split(" ")[1] for potcar in vasprun.potcar_symbols]
+ elif vasprun_obj and not path_to_vasprun:
+ potcar_names = [potcar.split(" ")[1] for potcar in vasprun_obj.potcar_symbols]
+ else:
+ raise ValueError(
+ "Please provide either path_to_potcar or list of "
+ "potcar_symbols or path to vasprun.xml or vasprun object. "
+ "Crucial to identify basis used for projections"
+ )
+
+ if path_to_poscar:
+ struct = Structure.from_file(path_to_poscar)
+ elif structure_obj:
+ struct = structure_obj
+ else:
+ raise ValueError("Please provide path_to_poscar or structure_obj")
+
+ ref_bases = Lobsterin.get_all_possible_basis_functions(structure=struct, potcar_symbols=potcar_names)
+
+ if path_to_lobsterin:
+ lobs_in = Lobsterin.from_file(path_to_lobsterin)
+ elif lobsterin_obj:
+ lobs_in = lobsterin_obj
+ else:
+ raise ValueError("Please provide path_to_lobsterin or lobsterin_obj")
+
+ calc_basis = []
+ for basis in lobs_in["basisfunctions"]:
+ basis_sep = basis.split()[1:]
+ basis_comb = " ".join(basis_sep)
+ calc_basis.append(basis_comb)
+
+ if calc_basis == list(ref_bases[0].values()):
+ quality_dict["minimal_basis"] = True # type: ignore
+ else:
+ quality_dict["minimal_basis"] = False # type: ignore
+ warnings.warn(
+ "Consider rerunning the calc with the minimum basis as well. Choosing is "
+ "larger basis set is recommended if you see a significant improvement of "
+ "the charge spilling and material has non-zero band gap.",
+ stacklevel=2,
+ )
+
+ if path_to_lobsterout:
+ lob_out = Lobsterout(path_to_lobsterout)
+ elif lobsterout_obj:
+ lob_out = lobsterout_obj
+ else:
+ raise ValueError("Please provide path_to_lobsterout or lobsterout_obj")
+
+ quality_dict["charge_spilling"] = {
+ "abs_charge_spilling": round((sum(lob_out.charge_spilling) / 2) * 100, 4),
+ "abs_total_spilling": round((sum(lob_out.total_spilling) / 2) * 100, 4),
+ } # type: ignore
+
+ if path_to_bandoverlaps is not None and not bandoverlaps_obj:
+ band_overlaps = Bandoverlaps(filename=path_to_bandoverlaps) if Path(path_to_bandoverlaps).exists() else None
+ elif path_to_bandoverlaps is None and bandoverlaps_obj:
+ band_overlaps = bandoverlaps_obj
+ else:
+ band_overlaps = None
+
+ if band_overlaps is not None:
+ for line in lob_out.warning_lines:
+ if "k-points could not be orthonormalized" in line:
+ total_kpoints = int(line.split(" ")[2])
+
+ # store actual number of devations above pymatgen default limit of 0.1
+ dev_val = []
+ for dev in band_overlaps.max_deviation:
+ if dev > 0.1:
+ dev_val.append(dev)
+
+ quality_dict["band_overlaps_analysis"] = { # type: ignore
+ "file_exists": True,
+ "limit_maxDeviation": 0.1,
+ "has_good_quality_maxDeviation": band_overlaps.has_good_quality_maxDeviation(limit_maxDeviation=0.1),
+ "max_deviation": round(max(band_overlaps.max_deviation), 4),
+ "percent_kpoints_abv_limit": round((len(dev_val) / total_kpoints) * 100, 4),
+ }
+
+ else:
+ quality_dict["band_overlaps_analysis"] = { # type: ignore
+ "file_exists": False,
+ "limit_maxDeviation": None,
+ "has_good_quality_maxDeviation": True,
+ "max_deviation": None,
+ "percent_kpoints_abv_limit": None,
+ }
+
+ if bva_comp:
+ try:
+ bond_valence = BVAnalyzer()
+
+ bva_oxi = []
+ if path_to_charge and not charge_obj:
+ lobs_charge = Charge(filename=path_to_charge)
+ elif not path_to_charge and charge_obj:
+ lobs_charge = charge_obj
+ else:
+ raise Exception("BVA comparison is requested, thus please provide path_to_charge or charge_obj")
+ for i in bond_valence.get_valences(structure=struct):
+ if i >= 0:
+ bva_oxi.append("POS")
+ else:
+ bva_oxi.append("NEG")
+
+ mull_oxi = []
+ for i in lobs_charge.Mulliken:
+ if i >= 0:
+ mull_oxi.append("POS")
+ else:
+ mull_oxi.append("NEG")
+
+ loew_oxi = []
+ for i in lobs_charge.Loewdin:
+ if i >= 0:
+ loew_oxi.append("POS")
+ else:
+ loew_oxi.append("NEG")
+
+ quality_dict["charge_comparisons"] = {} # type: ignore
+ if mull_oxi == bva_oxi:
+ quality_dict["charge_comparisons"]["bva_mulliken_agree"] = True # type: ignore
+ else:
+ quality_dict["charge_comparisons"]["bva_mulliken_agree"] = False # type: ignore
+
+ if mull_oxi == bva_oxi:
+ quality_dict["charge_comparisons"]["bva_loewdin_agree"] = True # type: ignore
+ else:
+ quality_dict["charge_comparisons"]["bva_loewdin_agree"] = False # type: ignore
+
+ except ValueError:
+ quality_dict["charge_comparisons"] = {} # type: ignore
+ warnings.warn(
+ "Oxidation states from BVA analyzer cannot be determined. "
+ "Thus BVA charge comparison will be skipped",
+ stacklevel=2,
+ )
+ if dos_comparison:
+ if "LSO" not in str(path_to_doscar).split("."):
+ warnings.warn(
+ "Consider using DOSCAR.LSO.lobster, as non LSO DOS from LOBSTER can have negative DOS values",
+ stacklevel=2,
+ )
+ if path_to_doscar:
+ doscar_lobster = Doscar(
+ doscar=path_to_doscar,
+ structure_file=path_to_poscar,
+ structure=structure_obj,
+ )
+
+ dos_lobster = doscar_lobster.completedos
+ elif lobster_completedos_obj:
+ dos_lobster = lobster_completedos_obj
+ else:
+ raise ValueError(
+ "Dos comparison is requested, so please provide either path_to_doscar or lobster_completedos_obj"
+ )
+
+ if path_to_vasprun:
+ vasprun = Vasprun(path_to_vasprun, parse_potcar_file=False, parse_eigen=False)
+ elif vasprun_obj:
+ vasprun = vasprun_obj
+ else:
+ raise ValueError(
+ "Dos comparison is requested, so please provide either path to vasprun.xml or vasprun_obj"
+ )
+ dos_vasp = vasprun.complete_dos
+
+ quality_dict["dos_comparisons"] = {} # type: ignore
+
+ for orb in dos_lobster.get_spd_dos():
+ if e_range[0] >= min(dos_vasp.energies) and e_range[0] >= min(dos_lobster.energies):
+ min_e = e_range[0]
+ else:
+ warnings.warn(
+ "Minimum energy range requested for DOS comparisons is not available "
+ "in VASP or LOBSTER calculation. Thus, setting min_e to -5 eV",
+ stacklevel=2,
+ )
+ min_e = -5
+
+ if e_range[-1] <= max(dos_vasp.energies) and e_range[-1] <= max(dos_lobster.energies):
+ max_e = e_range[-1]
+ else:
+ warnings.warn(
+ "Maximum energy range requested for DOS comparisons is not available "
+ "in VASP or LOBSTER calculation. Thus, setting max_e to 0 eV",
+ stacklevel=2,
+ )
+ max_e = 0
+
+ if np.diff(dos_vasp.energies)[0] >= 0.1 or np.diff(dos_lobster.energies)[0] >= 0.1:
+ warnings.warn(
+ "Input DOS files have very few points in the energy interval and thus "
+ "comparisons will not be reliable. Please rerun the calculations with "
+ "higher number of DOS points. Set NEDOS and COHPSteps tags to >= 2000 in VASP and LOBSTER "
+ "calculations, respectively.",
+ stacklevel=2,
+ )
+
+ if not n_bins:
+ n_bins = 56
+
+ fp_lobster_orb = dos_lobster.get_dos_fp(
+ min_e=min_e,
+ max_e=max_e,
+ n_bins=n_bins,
+ normalize=True,
+ type=orb.name,
+ )
+ fp_vasp_orb = dos_vasp.get_dos_fp(
+ min_e=min_e,
+ max_e=max_e,
+ n_bins=n_bins,
+ normalize=True,
+ type=orb.name,
+ )
+
+ tani_orb = round(
+ dos_vasp.get_dos_fp_similarity(fp_lobster_orb, fp_vasp_orb, tanimoto=True),
+ 4,
+ )
+ quality_dict["dos_comparisons"][f"tanimoto_orb_{orb.name}"] = tani_orb # type: ignore
+
+ fp_lobster = dos_lobster.get_dos_fp(
+ min_e=min_e,
+ max_e=max_e,
+ n_bins=n_bins,
+ normalize=True,
+ type="summed_pdos",
+ )
+ fp_vasp = dos_vasp.get_dos_fp(
+ min_e=min_e,
+ max_e=max_e,
+ n_bins=n_bins,
+ normalize=True,
+ type="summed_pdos",
+ )
+
+ tanimoto_summed = round(dos_vasp.get_dos_fp_similarity(fp_lobster, fp_vasp, tanimoto=True), 4)
+ quality_dict["dos_comparisons"]["tanimoto_summed"] = tanimoto_summed # type: ignore
+ quality_dict["dos_comparisons"]["e_range"] = [min_e, max_e] # type: ignore
+ quality_dict["dos_comparisons"]["n_bins"] = n_bins # type: ignore
+
+ return quality_dict
+
+
+
+# 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'.",
+ stacklevel=2,
+ )
+ 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))
+
+
+
+# Copyright (c) lobsterpy development team
+# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License
+
+"""This module defines wrapper classes to quickly obtain similarity matrix of input fingerprint objects."""
+
+from __future__ import annotations
+
+import multiprocessing as mp
+import os
+import warnings
+from pathlib import Path
+
+import numpy as np
+import pandas as pd
+from tqdm.autonotebook import tqdm
+
+from lobsterpy.featurize.core import (
+ CoxxFingerprint,
+ FeaturizeCharges,
+ FeaturizeCOXX,
+ FeaturizeDoscar,
+ FeaturizeLobsterpy,
+)
+from lobsterpy.structuregraph.graph import LobsterGraph
+
+from . import get_file_paths
+
+warnings.filterwarnings("ignore")
+
+
+
+[docs]
+class BatchSummaryFeaturizer:
+ """
+ Batch Featurizer sets that generates summary features from lobster data.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param path_to_jsons: path to root directory consisting of all lobster lightweight jsons
+ :param feature_type: set the feature type for moment features.
+ Possible options are `bonding`, `antibonding` or `overall`
+ :param charge_type: set charge type used for computing ionicity. Possible options are
+ `mulliken`, `loewdin` or `both`.
+ :param bonds: `all_bonds` or `cation_anion_bonds`
+ :param orbital_resolved: bool indicating whether LobsterPy analysis is performed orbital wise
+ :param include_cobi_data: bool stating to include COBICAR.lobster features
+ :param include_coop_data: bool stating to include COOPCAR.lobster features
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param n_jobs: parallel processes to run
+ """
+
+ def __init__(
+ self,
+ path_to_lobster_calcs: str | Path,
+ path_to_jsons: str | Path | None = None,
+ feature_type: str = "antibonding",
+ charge_type: str = "both",
+ bonds: str = "all",
+ orbital_resolved: bool = False,
+ include_cobi_data: bool = False,
+ include_coop_data: bool = False,
+ e_range: list[float] = [-5.0, 0.0],
+ n_jobs: int = 4,
+ ):
+ """
+ Featurize lobster data via multiprocessing for large number of compounds.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param path_to_jsons: path to root directory consisting of all lobster lightweight jsons
+ :param feature_type: set the feature type for moment features.
+ Possible options are `bonding`, `antibonding` or `overall`
+ :param charge_type: set charge type used for computing ionicity. Possible options are
+ `mulliken`, `loewdin` or `both`.
+ :param bonds: `all` or `cation-anion` bonds
+ :param orbital_resolved: bool indicating whether LobsterPy analysis is performed orbital wise
+ :param include_cobi_data: bool stating to include COBICAR.lobster features
+ :param include_coop_data: bool stating to include COOPCAR.lobster features
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param n_jobs: parallel processes to run
+ """
+ # Check for valid parameters of string type
+ allowed_str_inputs = {
+ "charge_type": ["mulliken", "loewdin", "both"],
+ "bonds": ["all", "cation-anion"],
+ "feature_type": ["bonding", "antibonding", "overall"],
+ }
+ for param, param_string in zip([charge_type, bonds, feature_type], ["charge_type", "bonds", "feature_type"]):
+ if param not in allowed_str_inputs[param_string]:
+ raise ValueError(
+ f"Parameter {param_string} set to {param} but must be in "
+ f"{list(allowed_str_inputs[param_string])}."
+ )
+
+ self.path_to_lobster_calcs = path_to_lobster_calcs
+ self.path_to_jsons = path_to_jsons
+ self.feature_type = feature_type
+ self.charge_type = charge_type
+ self.bonds = bonds
+ self.orbital_resolved = orbital_resolved
+ self.include_cobi_data = include_cobi_data
+ self.include_coop_data = include_coop_data
+ self.e_range = e_range
+ self.n_jobs = n_jobs
+
+ def _featurizelobsterpy(self, file_name_or_path: str | Path) -> pd.DataFrame:
+ """
+ Featurize Lobsterpy condensed bonding analysis data.
+
+ if lightweight json file exists loads that or invokes LobsterPy Analysis class.
+
+ :param file_name_or_path: path to the LOBSTER calc directory or
+ lightweight condensed bonding analysis json file name.
+
+ Returns:
+ A pandas dataframe with ICOHP stats like mean, min, max of relevant bonds and
+ madelung energies
+
+ """
+ if Path(file_name_or_path).is_file():
+ featurize_lobsterpy = FeaturizeLobsterpy(
+ path_to_json=file_name_or_path,
+ bonds=self.bonds,
+ )
+
+ else:
+ featurize_lobsterpy = FeaturizeLobsterpy(
+ path_to_lobster_calc=file_name_or_path,
+ bonds=self.bonds,
+ orbital_resolved=self.orbital_resolved,
+ )
+
+ return featurize_lobsterpy.get_df()
+
+ def _featurizecoxx(self, path_to_lobster_calc: str | Path) -> pd.DataFrame:
+ """
+ Featurize COHP/COBI/COOPCAR data using FeaturizeCOXX.
+
+ :param path_to_lobster_calc: path to root LOBSTER calc directory
+
+ Returns:
+ A pandas dataframe with COHP summary stats data mainly weighted ICOHP/ICOOP/ICOBI,
+ Effective interaction number and moment features (center, width, skewness and kurtosis)
+
+ """
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["structure", "cohpcar", "icohplist"]
+ )
+ structure_path = file_paths.get("structure")
+
+ coxx = FeaturizeCOXX(
+ path_to_coxxcar=str(file_paths.get("cohpcar")),
+ path_to_icoxxlist=str(file_paths.get("icohplist")),
+ path_to_structure=str(structure_path),
+ feature_type=self.feature_type,
+ e_range=self.e_range,
+ )
+
+ df = coxx.get_summarized_coxx_df()
+ del coxx
+
+ if self.include_cobi_data:
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["cobicar", "icobilist"]
+ )
+
+ coxx = FeaturizeCOXX(
+ path_to_coxxcar=str(file_paths.get("cobicar")),
+ path_to_icoxxlist=str(file_paths.get("icobilist")),
+ path_to_structure=str(structure_path),
+ feature_type=self.feature_type,
+ e_range=self.e_range,
+ are_cobis=True,
+ )
+
+ df_cobi = coxx.get_summarized_coxx_df()
+ df = pd.concat([df, df_cobi], axis=1)
+ del coxx
+
+ if self.include_coop_data:
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["coopcar", "icooplist"]
+ )
+
+ coxx = FeaturizeCOXX(
+ path_to_coxxcar=str(file_paths.get("coopcar")),
+ path_to_icoxxlist=str(file_paths.get("icooplist")),
+ path_to_structure=str(structure_path),
+ feature_type=self.feature_type,
+ e_range=self.e_range,
+ are_coops=True,
+ )
+
+ df_coop = coxx.get_summarized_coxx_df()
+ df = pd.concat([df, df_coop], axis=1)
+ del coxx
+
+ return df
+
+ def _featurizecharges(self, path_to_lobster_calc: str | Path) -> pd.DataFrame:
+ """
+ Featurize CHARGE.lobster.gz data that using FeaturizeCharges.
+
+ :param path_to_lobster_calc: path to root LOBSTER calc directory
+
+ Returns:
+ A pandas dataframe with computed ionicity for the structure
+
+ """
+ file_paths = get_file_paths(path_to_lobster_calc=path_to_lobster_calc, requested_files=["structure", "charge"])
+
+ if self.charge_type == "mulliken":
+ charge_mull = FeaturizeCharges(
+ path_to_charge=str(file_paths.get("charge")),
+ path_to_structure=str(file_paths.get("structure")),
+ charge_type="mulliken",
+ )
+ df = charge_mull.get_df()
+ elif self.charge_type == "loewdin":
+ charge_loew = FeaturizeCharges(
+ path_to_charge=str(file_paths.get("charge")),
+ path_to_structure=str(file_paths.get("structure")),
+ charge_type="loewdin",
+ )
+ df = charge_loew.get_df()
+ else:
+ charge_mull = FeaturizeCharges(
+ path_to_charge=str(file_paths.get("charge")),
+ path_to_structure=str(file_paths.get("structure")),
+ charge_type="mulliken",
+ )
+ df_mull = charge_mull.get_df()
+
+ charge_loew = FeaturizeCharges(
+ path_to_charge=str(file_paths.get("charge")),
+ path_to_structure=str(file_paths.get("structure")),
+ charge_type="loewdin",
+ )
+ df_loew = charge_loew.get_df()
+
+ df = pd.concat([df_mull, df_loew], axis=1)
+
+ return df
+
+
+[docs]
+ def get_df(self) -> pd.DataFrame:
+ """
+ Generate a pandas dataframe with summary features extracted from LOBSTER files.
+
+ Uses multiprocessing to speed up the process.
+
+ Returns:
+ Returns a pandas dataframe
+
+ """
+ if self.path_to_jsons:
+ file_name_or_path = [
+ os.path.join(self.path_to_jsons, f)
+ for f in os.listdir(self.path_to_jsons)
+ if not f.startswith("t") and not f.startswith(".") and not os.path.isdir(f)
+ ]
+
+ elif self.path_to_lobster_calcs and not self.path_to_jsons:
+ file_name_or_path = [
+ os.path.join(self.path_to_lobster_calcs, f)
+ for f in os.listdir(self.path_to_lobster_calcs)
+ if not f.startswith("t")
+ and not f.startswith(".")
+ and os.path.isdir(os.path.join(self.path_to_lobster_calcs, f))
+ ]
+
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(total=len(file_name_or_path), desc="Generating LobsterPy summary stats") as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._featurizelobsterpy, file_name_or_path, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df_lobsterpy = pd.concat(row)
+ df_lobsterpy.sort_index(inplace=True) # noqa: PD002
+
+ paths = [
+ os.path.join(self.path_to_lobster_calcs, f)
+ for f in os.listdir(self.path_to_lobster_calcs)
+ if not f.startswith("t")
+ and not f.startswith(".")
+ and os.path.isdir(os.path.join(self.path_to_lobster_calcs, f))
+ ]
+
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(total=len(paths), desc="Generating COHP/COOP/COBI summary stats") as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._featurizecoxx, paths, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df_coxx = pd.concat(row)
+ df_coxx.sort_index(inplace=True) # noqa: PD002
+
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(total=len(paths), desc="Generating charge based features") as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._featurizecharges, paths, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df_charges = pd.concat(row)
+ df_charges.sort_index(inplace=True) # noqa: PD002
+
+ return pd.concat([df_lobsterpy, df_coxx, df_charges], axis=1)
+
+
+
+
+
+[docs]
+class BatchCoxxFingerprint:
+ """
+ BatchFeaturizer to generate COHP/COOP/COBI fingerprints and Tanimoto index similarity matrix.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param feature_type: set the feature type for moment features.
+ Possible options are `bonding`, `antibonding` or `overall`
+ :param label_list: bond labels list for which fingerprints needs to be generated.
+ :param tanimoto: bool to state to compute tanimoto index between fingerprint objects
+ :param normalize: bool to state to normalize the fingerprint data
+ :param spin_type: can be `summed` or `up` or `down`.
+ :param n_bins: sets number for bins for fingerprint objects
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param n_jobs: number of parallel processes to run
+ :param fingerprint_for: Possible options are `cohp` or `cobi` or `coop`.
+ Based on this fingerprints will be computed for COHPCAR/COOBICAR/COOPCAR.lobster files
+
+ Attributes:
+ fingerprint_df: A pandas dataframe with fingerprint objects
+ """
+
+ def __init__(
+ self,
+ path_to_lobster_calcs: str | Path,
+ feature_type: str = "overall",
+ label_list: list[str] | None = None,
+ tanimoto: bool = True,
+ normalize: bool = True,
+ spin_type: str = "summed",
+ n_bins: int = 56,
+ e_range: list[float] = [-15.0, 0.0],
+ n_jobs=4,
+ fingerprint_for: str = "cohp",
+ ):
+ """
+ Generate COHP/COOP/COBI fingerprints and pair-wise Tanimoto index similarity matrix.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param feature_type: set the feature type for moment features.
+ Possible options are `bonding`, `antibonding` or `overall`
+ :param label_list: bond labels list for which fingerprints needs to be generated.
+ :param tanimoto: bool to state to compute tanimoto index between fingerprint objects
+ :param normalize: bool to state to normalize the fingerprint data
+ :param spin_type: can be `summed` or `up` or `down`.
+ :param n_bins: sets number for bins for fingerprint objects
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param n_jobs: number of parallel processes to run
+ :param fingerprint_for: Possible options are `cohp` or `cobi` or `coop`.
+ Based on this fingerprints will be computed for COHPCAR/COOBICAR/COOPCAR.lobster files
+ """
+ self.path_to_lobster_calcs = path_to_lobster_calcs
+ self.feature_type = feature_type
+ self.tanimoto = tanimoto
+ self.normalize = normalize
+ self.label_list = label_list
+ self.spin_type = spin_type
+ self.n_bins = n_bins
+ self.e_range = e_range
+ self.n_jobs = n_jobs
+ self.fingerprint_for = fingerprint_for
+
+ self.fingerprint_df = self._get_fingerprints_df()
+
+
+[docs]
+ def get_similarity_matrix_df(self) -> pd.DataFrame:
+ """
+ Compute pairwise similarity index for each fingerprint object in input dataframe.
+
+ Returns:
+ A Pandas dataframe
+ """
+ matrix = np.full((self.fingerprint_df.shape[0], self.fingerprint_df.shape[0]), np.nan)
+ for i, (_, col) in enumerate(self.fingerprint_df.iterrows()):
+ for j, (_, col1) in enumerate(self.fingerprint_df.iterrows()):
+ if self.tanimoto:
+ simi = self._get_fp_similarity(
+ col["COXX_FP"],
+ col1["COXX_FP"],
+ tanimoto=self.tanimoto,
+ normalize=False,
+ )
+ else:
+ simi = self._get_fp_similarity(
+ col["COXX_FP"],
+ col1["COXX_FP"],
+ tanimoto=False,
+ normalize=True,
+ )
+ matrix[i][j] = simi
+
+ return pd.DataFrame(
+ matrix,
+ index=list(self.fingerprint_df.index),
+ columns=list(self.fingerprint_df.index),
+ )
+
+
+ @staticmethod
+ def _fp_to_dict(fp: CoxxFingerprint) -> dict:
+ """
+ Convert a fingerprint obj into a dictionary.
+
+ :param fp: The fingerprint to be converted into a dictionary
+
+ Returns:
+ dict: A dict of the fingerprint Keys=type, Values=np.ndarray(energies, cohp)
+ """
+ fp_dict = {}
+ fp_dict[fp[2]] = np.array([fp[0], fp[1]], dtype="object").T
+
+ return fp_dict
+
+ @staticmethod
+ def _get_fp_similarity(
+ fp1: CoxxFingerprint,
+ fp2: CoxxFingerprint,
+ col: int = 1,
+ pt: int | str = "All",
+ normalize: bool = False,
+ tanimoto: bool = True,
+ ) -> float:
+ """
+ Calculate the similarity index (dot product) of two fingerprints.
+
+ :param fp1 The 1st CoxxFingerprint object
+ :param fp2: The 2nd CoxxFingerprint object
+ :param col: The item in the fingerprints (0:energies,1: coxxs) to take the dot product of (default is 1)
+ :param pt: The index of the point that the dot product is to be taken (default is All)
+ :param normalize: If True normalize the scalar product to 1 (default is False)
+ :param tanimoto: If True will compute Tanimoto index (default is False)
+
+ Raises:
+ ValueError: If both tanimoto and normalize are set to True.
+
+ Returns:
+ Similarity index (float): The value of dot product
+
+ """
+ fp1_dict = BatchCoxxFingerprint._fp_to_dict(fp1) if not isinstance(fp1, dict) else fp1
+
+ fp2_dict = BatchCoxxFingerprint._fp_to_dict(fp2) if not isinstance(fp2, dict) else fp2
+
+ if pt == "All":
+ vec1 = np.array([pt[col] for pt in fp1_dict.values()]).flatten()
+ vec2 = np.array([pt[col] for pt in fp2_dict.values()]).flatten()
+ else:
+ vec1 = fp1_dict[fp1[2][pt]][col] # type: ignore
+ vec2 = fp2_dict[fp2[2][pt]][col] # type: ignore
+
+ if not normalize and tanimoto:
+ rescale = np.linalg.norm(vec1) ** 2 + np.linalg.norm(vec2) ** 2 - np.dot(vec1, vec2)
+
+ elif not tanimoto and normalize:
+ rescale = np.linalg.norm(vec1) * np.linalg.norm(vec2)
+
+ elif not tanimoto and not normalize:
+ rescale = 1.0
+
+ else:
+ raise ValueError(
+ "Cannot compute similarity index. Please set either normalize=True or tanimoto=True or both to False."
+ )
+ return np.dot(vec1, vec2) / rescale
+
+ def _fingerprint_df(self, path_to_lobster_calc: str | Path) -> pd.DataFrame:
+ """
+ Get fingerprint object dataframe via FeaturizeCOXX.get_coxx_fingerprint_df.
+
+ Also helps to generate the data used for fingerprint generation.
+
+ :param path_to_lobster_calc: path to root LOBSTER calculation directory.
+
+ Returns:
+ A pandas dataframe with COXX fingerprint object
+
+ """
+ if self.fingerprint_for.upper() == "COBI":
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["structure", "cobicar", "icobilist"]
+ )
+
+ coxxcar_path = file_paths.get("cobicar")
+ icoxxlist_path = file_paths.get("icobilist")
+ are_cobis = True
+ are_coops = False
+
+ elif self.fingerprint_for.upper() == "COOP":
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["structure", "coopcar", "icooplist"]
+ )
+
+ coxxcar_path = file_paths.get("coopcar")
+ icoxxlist_path = file_paths.get("icooplist")
+ are_cobis = False
+ are_coops = True
+
+ else:
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["structure", "cohpcar", "icohplist"]
+ )
+
+ coxxcar_path = file_paths.get("cohpcar")
+ icoxxlist_path = file_paths.get("icohplist")
+ are_cobis = False
+ are_coops = False
+
+ coxx = FeaturizeCOXX(
+ path_to_coxxcar=str(coxxcar_path),
+ path_to_icoxxlist=str(icoxxlist_path),
+ path_to_structure=str(file_paths.get("structure")),
+ feature_type=self.feature_type,
+ e_range=self.e_range,
+ are_coops=are_coops,
+ are_cobis=are_cobis,
+ )
+
+ return coxx.get_coxx_fingerprint_df(
+ spin_type=self.spin_type,
+ n_bins=self.n_bins,
+ normalize=self.normalize,
+ label_list=self.label_list,
+ )
+
+ def _get_fingerprints_df(self) -> pd.DataFrame:
+ """
+ Generate fingerprint objects dataframe using BatchCoxxFingerprint._fingerprint_df.
+
+ Returns:
+ A pandas dataframe with COXX fingerprint objects
+
+ """
+ paths = [
+ os.path.join(self.path_to_lobster_calcs, f)
+ for f in os.listdir(self.path_to_lobster_calcs)
+ if not f.startswith("t")
+ and not f.startswith(".")
+ and os.path.isdir(os.path.join(self.path_to_lobster_calcs, f))
+ ]
+
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(
+ total=len(paths),
+ desc=f"Generating {self.fingerprint_for.upper()} fingerprints",
+ ) as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._fingerprint_df, paths, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df = pd.concat(row)
+ df.sort_index(inplace=True) # noqa: PD002
+
+ return df
+
+
+
+
+[docs]
+class BatchStructureGraphs:
+ """
+ Batch Featurizer that generates structure graphs with lobster data.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param add_additional_data_sg: bool indicating whether to include `icoop` and `icobi` data as edge properties
+ :param which_bonds: selects which kind of bonds are analyzed. "all" is the default
+ :param cutoff_icohp: only bonds that are stronger than cutoff_icohp * strongest ICOHP will be considered.
+ :param noise_cutoff: if provided hardcodes the lower limit of icohps considered.
+ :param start: start energy for bonding antibonding percent integration
+ :param n_jobs: parallel processes to run
+
+ """
+
+ def __init__(
+ self,
+ path_to_lobster_calcs: str | Path,
+ add_additional_data_sg: bool = True,
+ which_bonds: str = "all",
+ cutoff_icohp: float = 0.10,
+ noise_cutoff: float = 0.1,
+ start: float | None = None,
+ n_jobs: int = 4,
+ ):
+ """
+ Generate structure graphs with LOBSTER data via multiprocessing.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param add_additional_data_sg: bool indicating whether to include `icoop` and `icobi` data as edge properties
+ :param which_bonds: selects which kind of bonds are analyzed. "all" is the default
+ :param cutoff_icohp: only bonds that are stronger than cutoff_icohp * strongest ICOHP will be considered.
+ :param noise_cutoff: if provided hardcodes the lower limit of icohps considered.
+ :param start: start energy for bonding antibonding percent integration
+ :param n_jobs: parallel processes to run
+
+ """
+ self.path_to_lobster_calcs = path_to_lobster_calcs
+ self.add_additional_data_sg = add_additional_data_sg
+ self.which_bonds = which_bonds
+ self.cutff_icohp = cutoff_icohp
+ self.noise_cutoff = noise_cutoff
+ self.start = start
+ self.n_jobs = n_jobs
+
+ def _get_sg_df(self, path_to_lobster_calc: str | Path) -> pd.DataFrame:
+ """
+ Generate a structure graph with LOBSTER data bonding analysis data.
+
+ :param path_to_lobster_calc: path to root LOBSTER calculation directory
+
+ Returns:
+ A structure graph with LOBSTER data as edge and node properties in structure graph objects
+ """
+ dir_name = Path(path_to_lobster_calc)
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc,
+ requested_files=["charge", "cohpcar", "icohplist", "icooplist", "icobilist", "madelung", "structure"],
+ )
+
+ graph = LobsterGraph(
+ path_to_poscar=str(file_paths.get("structure")),
+ path_to_charge=str(file_paths.get("charge")),
+ path_to_cohpcar=str(file_paths.get("cohpcar")),
+ path_to_icohplist=str(file_paths.get("icohplist")),
+ add_additional_data_sg=self.add_additional_data_sg,
+ path_to_icooplist=str(file_paths.get("icooplist")),
+ path_to_icobilist=str(file_paths.get("icobilist")),
+ path_to_madelung=str(file_paths.get("madelung")),
+ which_bonds=self.which_bonds,
+ cutoff_icohp=self.cutff_icohp,
+ noise_cutoff=self.noise_cutoff,
+ start=self.start,
+ )
+
+ ids = dir_name.name
+
+ df = pd.DataFrame(index=[ids])
+
+ df.loc[ids, "structure_graph"] = graph.sg
+
+ return df
+
+
+[docs]
+ def get_df(self) -> pd.DataFrame:
+ """
+ Generate a pandas dataframe with structure graph with LOBSTER data.
+
+ Uses multiprocessing to speed up the process.
+
+ Returns:
+ Returns a pandas dataframe
+
+ """
+ paths = [
+ os.path.join(self.path_to_lobster_calcs, f)
+ for f in os.listdir(self.path_to_lobster_calcs)
+ if not f.startswith("t")
+ and not f.startswith(".")
+ and os.path.isdir(os.path.join(self.path_to_lobster_calcs, f))
+ ]
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(total=len(paths), desc="Generating Structure Graphs") as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._get_sg_df, paths, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df_sg = pd.concat(row)
+ df_sg.sort_index(inplace=True) # noqa: PD002
+
+ return df_sg
+
+
+
+
+
+[docs]
+class BatchDosFeaturizer:
+ """
+ BatchFeaturizer to generate Lobster DOS moment features and fingerprints.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param add_element_dos_moments: add element dos moment features alongside orbital dos
+ :param normalize: bool to state to normalize the fingerprint data
+ :param n_bins: sets number for bins for fingerprint objects
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param n_jobs: number of parallel processes to run
+ :param fingerprint_type: Specify fingerprint type to compute, can accept `{s/p/d/f/}summed_{pdos/tdos}`
+ (default is summed_pdos)
+ :param use_lso_dos: Will force featurizer to use DOSCAR.LSO.lobster instead of DOSCAR.lobster
+
+ """
+
+ def __init__(
+ self,
+ path_to_lobster_calcs: str | Path,
+ add_element_dos_moments: bool = False,
+ fingerprint_type: str = "summed_pdos",
+ normalize: bool = True,
+ n_bins: int = 56,
+ e_range: list[float] = [-15.0, 0.0],
+ n_jobs=4,
+ use_lso_dos: bool = True,
+ ):
+ """
+ Initialize BatchDosFeaturizer.
+
+ :param path_to_lobster_calcs: path to root directory consisting of all lobster calc
+ :param add_element_dos_moments: add element dos moment features alongside orbital dos
+ :param normalize: bool to state to normalize the fingerprint data
+ :param n_bins: sets number for bins for fingerprint objects
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param n_jobs: number of parallel processes to run
+ :param fingerprint_type: Specify fingerprint type to compute, can accept `{s/p/d/f/}summed_{pdos/tdos}`
+ (default is summed_pdos)
+ :param use_lso_dos: Will force featurizer to use DOSCAR.LSO.lobster instead of DOSCAR.lobster
+ """
+ self.path_to_lobster_calcs = path_to_lobster_calcs
+ self.add_element_dos_moments = add_element_dos_moments
+ self.fingerprint_type = fingerprint_type
+ self.e_range = e_range
+ self.normalize = normalize
+ self.n_jobs = n_jobs
+ self.n_bins = n_bins
+ self.use_lso_dos = use_lso_dos
+
+ def _get_dos_moments_df(self, path_to_lobster_calc: str | Path) -> pd.DataFrame:
+ """
+ Featurize DOSCAR.lobster data using FeaturizeDOSCAR.
+
+ Returns:
+ A pandas dataframe with computed PDOS moment features
+ """
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc,
+ requested_files=["structure", "doscar"],
+ use_lso_dos=self.use_lso_dos,
+ )
+
+ featurize_dos = FeaturizeDoscar(
+ path_to_doscar=str(file_paths.get("doscar")),
+ path_to_structure=str(file_paths.get("structure")),
+ add_element_dos_moments=self.add_element_dos_moments,
+ e_range=self.e_range,
+ )
+
+ return featurize_dos.get_df()
+
+ def _get_dos_fingerprints_df(self, path_to_lobster_calc: str | Path) -> pd.DataFrame:
+ """
+ Featurize DOSCAR.lobster data into fingerprints using FeaturizeDOSCAR.
+
+ :param path_to_lobster_calc: path to root LOBSTER calculation directory.
+
+ Returns:
+ A pandas dataframe with DOS fingerprint objects
+ """
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc,
+ requested_files=["structure", "doscar"],
+ use_lso_dos=self.use_lso_dos,
+ )
+
+ featurize_dos = FeaturizeDoscar(
+ path_to_doscar=str(file_paths.get("doscar")),
+ path_to_structure=str(file_paths.get("structure")),
+ e_range=self.e_range,
+ )
+
+ return featurize_dos.get_fingerprint_df(
+ fp_type=self.fingerprint_type,
+ normalize=self.normalize,
+ n_bins=self.n_bins,
+ )
+
+
+[docs]
+ def get_df(self) -> pd.DataFrame:
+ """
+ Generate a pandas dataframe with all moment features.
+
+ Moment features are PDOS (optional: element dos) center, width, skewness, kurtosis
+ and upper band edge.
+
+ Returns:
+ A pandas dataframe with moment features
+ """
+ paths = [
+ os.path.join(self.path_to_lobster_calcs, f)
+ for f in os.listdir(self.path_to_lobster_calcs)
+ if not f.startswith("t")
+ and not f.startswith(".")
+ and os.path.isdir(os.path.join(self.path_to_lobster_calcs, f))
+ ]
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(total=len(paths), desc="Generating PDOS moment features") as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._get_dos_moments_df, paths, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df_dos = pd.concat(row)
+ df_dos.sort_index(inplace=True) # noqa: PD002
+
+ return df_dos
+
+
+
+[docs]
+ def get_fingerprints_df(self) -> pd.DataFrame:
+ """
+ Generate a pandas dataframe with DOS fingerprints.
+
+ Returns:
+ A pandas dataframe with fingerprint objects
+ """
+ paths = [
+ os.path.join(self.path_to_lobster_calcs, f)
+ for f in os.listdir(self.path_to_lobster_calcs)
+ if not f.startswith("t")
+ and not f.startswith(".")
+ and os.path.isdir(os.path.join(self.path_to_lobster_calcs, f))
+ ]
+ row = []
+ with (
+ mp.Pool(processes=self.n_jobs, maxtasksperchild=1) as pool,
+ tqdm(total=len(paths), desc="Generating DOS fingerprints") as pbar,
+ ):
+ for _, result in enumerate(pool.imap_unordered(self._get_dos_fingerprints_df, paths, chunksize=1)):
+ pbar.update()
+ row.append(result)
+
+ df_dos_fp = pd.concat(row)
+ df_dos_fp.sort_index(inplace=True) # noqa: PD002
+
+ return df_dos_fp
+
+
+
+# Copyright (c) lobsterpy development team
+# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License
+
+"""This module defines classes to featurize Lobster data ready to be used for ML studies."""
+
+from __future__ import annotations
+
+import gzip
+import json
+import warnings
+from pathlib import Path
+from typing import NamedTuple
+
+import numpy as np
+import pandas as pd
+from mendeleev import element
+from numpy import ndarray
+from pymatgen.core.structure import Structure
+from pymatgen.electronic_structure.cohp import CompleteCohp
+from pymatgen.electronic_structure.core import Spin
+from pymatgen.io.lobster import Charge, Doscar, Icohplist, MadelungEnergies
+from scipy.integrate import trapezoid
+from scipy.signal import hilbert
+
+from lobsterpy.cohp.analyze import Analysis
+
+from . import get_file_paths
+
+warnings.filterwarnings("ignore")
+
+
+
+[docs]
+class FeaturizeLobsterpy:
+ """
+ Class to featurize lobsterpy data.
+
+ :param path_to_lobster_calc: path to parent directory containing lobster calc outputs
+ :param path_to_json: path to lobster lightweight json
+ :param bonds: "all" or "cation-anion" bonds
+ :param orbital_resolved: bool indicating whether LobsterPy analysis is performed orbital wise
+ """
+
+ def __init__(
+ self,
+ path_to_lobster_calc: str | Path | None = None,
+ path_to_json: str | Path | None = None,
+ orbital_resolved: bool = False,
+ bonds: str = "all",
+ ):
+ """Initialize featurizer."""
+ self.path_to_json = path_to_json
+ self.path_to_lobster_calc = path_to_lobster_calc
+ self.orbital_resolved = orbital_resolved
+ self.bonds = bonds
+
+
+[docs]
+ def get_df(self, ids: str | None = None) -> pd.DataFrame:
+ """
+ Featurize LobsterPy condensed bonding analysis data.
+
+ :param ids: set index name in the pandas dataframe. Default is None.
+ When None, LOBSTER calc directory name is used as index name.
+
+ Returns:
+ Returns a pandas dataframe with lobsterpy icohp statistics
+ """
+ if self.path_to_json and not self.path_to_lobster_calc:
+ # read the lightweight lobster json files using read_lobster_lightweight_json method
+ data = FeaturizeLobsterpy.read_lobster_lightweight_json(path_to_json=self.path_to_json)
+ if not ids:
+ ids = Path(self.path_to_json).name.split(".")[0]
+
+ elif self.path_to_lobster_calc and not self.path_to_json:
+ # get lobsterpy condensed bonding analysis data using get_lobsterpy_cba_dict method
+ data = FeaturizeLobsterpy.get_lobsterpy_cba_dict(
+ path_to_lobster_calc=self.path_to_lobster_calc,
+ bonds=self.bonds,
+ orbital_resolved=self.orbital_resolved,
+ )
+
+ if not ids:
+ ids = Path(self.path_to_lobster_calc).name
+
+ else:
+ raise ValueError("Please provide either path to lightweight lobster jsons or path to lobster calc")
+ # define a pandas dataframe
+ df = pd.DataFrame(index=[ids])
+
+ icohp_mean = []
+ icohp_sum = []
+ icohp_mean_orb_bndg = []
+ icohp_sum_orb_bndg = []
+ icohp_mean_orb_antibndg = []
+ icohp_sum_orb_antibndg = []
+ bond_orb = []
+ antibond_orb = []
+ bond = []
+ antibond = []
+
+ # extract lobsterpy icohp related data for bond type specified
+
+ # Results will differ for "all" and "cation-anion" mode.
+
+ # In "all" bonds mode, the bonds will come up twice, also
+ # cation-cation, anion-anion bonds will also be considered
+
+ if self.bonds == "all":
+ bond_type = "all_bonds"
+ elif self.bonds == "cation-anion":
+ bond_type = "cation_anion_bonds"
+
+ if bond_type == "cation_anion_bonds" and (
+ not data[bond_type]
+ or not data[bond_type]["lobsterpy_data"]
+ or not data[bond_type]["lobsterpy_data"]["sites"]
+ ):
+ raise Exception(f"No {self.bonds} bonds detected for {ids} structure. Please switch to `all` bonds mode")
+
+ if data[bond_type]["lobsterpy_data"]:
+ for site_data in data[bond_type]["lobsterpy_data"]["sites"].values():
+ if site_data["bonds"]:
+ for bond_data in site_data["bonds"].values():
+ icohp_mean.append(float(bond_data["ICOHP_mean"]))
+ icohp_sum.append(float(bond_data["ICOHP_sum"]))
+ bond.append(bond_data["bonding"]["perc"])
+ antibond.append(bond_data["antibonding"]["perc"])
+ if self.orbital_resolved:
+ if (
+ bond_data["orbital_data"]["orbital_summary_stats"]
+ and "max_bonding_contribution" in bond_data["orbital_data"]["orbital_summary_stats"]
+ ):
+ for orb_pair in bond_data["orbital_data"]["orbital_summary_stats"][
+ "max_bonding_contribution"
+ ]:
+ icohp_mean_orb_bndg.append(bond_data["orbital_data"][orb_pair]["ICOHP_mean"])
+ icohp_sum_orb_bndg.append(bond_data["orbital_data"][orb_pair]["ICOHP_sum"])
+ bond_orb.append(
+ bond_data["orbital_data"][orb_pair]["orb_contribution_perc_bonding"]
+ )
+ if (
+ bond_data["orbital_data"]["orbital_summary_stats"]
+ and "max_antibonding_contribution" in bond_data["orbital_data"]["orbital_summary_stats"]
+ ):
+ for orb_pair in bond_data["orbital_data"]["orbital_summary_stats"][
+ "max_antibonding_contribution"
+ ]:
+ icohp_mean_orb_antibndg.append(bond_data["orbital_data"][orb_pair]["ICOHP_mean"])
+ icohp_sum_orb_antibndg.append(bond_data["orbital_data"][orb_pair]["ICOHP_sum"])
+ antibond_orb.append(
+ bond_data["orbital_data"][orb_pair]["orb_contribution_perc_antibonding"]
+ )
+
+ # add ICOHP stats data (mean, min, max, standard deviation) as columns to the dataframe
+
+ df.loc[ids, "Icohp_mean_avg"] = 0 if len(icohp_mean) == 0 else np.mean(icohp_mean)
+ df.loc[ids, "Icohp_mean_max"] = 0 if len(icohp_mean) == 0 else np.max(icohp_mean)
+ df.loc[ids, "Icohp_mean_min"] = 0 if len(icohp_mean) == 0 else np.min(icohp_mean)
+ df.loc[ids, "Icohp_mean_std"] = 0 if len(icohp_mean) == 0 else np.std(icohp_mean)
+
+ df.loc[ids, "Icohp_sum_avg"] = 0 if len(icohp_sum) == 0 else np.mean(icohp_sum)
+ df.loc[ids, "Icohp_sum_max"] = 0 if len(icohp_sum) == 0 else np.max(icohp_sum)
+ df.loc[ids, "Icohp_sum_min"] = 0 if len(icohp_sum) == 0 else np.min(icohp_sum)
+ df.loc[ids, "Icohp_sum_std"] = 0 if len(icohp_sum) == 0 else np.std(icohp_sum)
+
+ df.loc[ids, "bonding_perc_avg"] = 0 if len(bond) == 0 else np.mean(bond)
+ df.loc[ids, "bonding_perc_max"] = 0 if len(bond) == 0 else np.max(bond)
+ df.loc[ids, "bonding_perc_min"] = 0 if len(bond) == 0 else np.min(bond)
+ df.loc[ids, "bonding_perc_std"] = 0 if len(bond) == 0 else np.std(bond)
+
+ df.loc[ids, "antibonding_perc_avg"] = 0 if len(antibond) == 0 else np.mean(antibond)
+ df.loc[ids, "antibonding_perc_min"] = 0 if len(antibond) == 0 else np.min(antibond)
+ df.loc[ids, "antibonding_perc_max"] = 0 if len(antibond) == 0 else np.max(antibond)
+ df.loc[ids, "antibonding_perc_std"] = 0 if len(antibond) == 0 else np.std(antibond)
+
+ if self.orbital_resolved:
+ # bonding orbital
+ df.loc[ids, "Icohp_bndg_orb_mean_avg"] = (
+ 0 if len(icohp_mean_orb_bndg) == 0 else np.mean(icohp_mean_orb_bndg)
+ )
+ df.loc[ids, "Icohp_bndg_orb_mean_max"] = 0 if len(icohp_mean_orb_bndg) == 0 else np.max(icohp_mean_orb_bndg)
+ df.loc[ids, "Icohp_bndg_orb_mean_min"] = 0 if len(icohp_mean_orb_bndg) == 0 else np.min(icohp_mean_orb_bndg)
+ df.loc[ids, "Icohp_bndg_orb_mean_std"] = 0 if len(icohp_mean_orb_bndg) == 0 else np.std(icohp_mean_orb_bndg)
+
+ df.loc[ids, "Icohp_bndg_orb_sum_avg"] = 0 if len(icohp_sum_orb_bndg) == 0 else np.mean(icohp_sum_orb_bndg)
+ df.loc[ids, "Icohp_bndg_orb_sum_max"] = 0 if len(icohp_sum_orb_bndg) == 0 else np.max(icohp_sum_orb_bndg)
+ df.loc[ids, "Icohp_bndg_orb_sum_min"] = 0 if len(icohp_sum_orb_bndg) == 0 else np.min(icohp_sum_orb_bndg)
+ df.loc[ids, "Icohp_bndg_orb_sum_std"] = 0 if len(icohp_sum_orb_bndg) == 0 else np.std(icohp_sum_orb_bndg)
+
+ df.loc[ids, "bonding_orb_perc_avg"] = 0 if len(bond_orb) == 0 else np.mean(bond_orb)
+ df.loc[ids, "bonding_orb_perc_max"] = 0 if len(bond_orb) == 0 else np.max(bond_orb)
+ df.loc[ids, "bonding_orb_perc_min"] = 0 if len(bond_orb) == 0 else np.min(bond_orb)
+ df.loc[ids, "bonding_orb_perc_std"] = 0 if len(bond_orb) == 0 else np.std(bond_orb)
+
+ # anti-bonding orbital
+ df.loc[ids, "Icohp_antibndg_orb_mean_avg"] = (
+ 0 if len(icohp_mean_orb_antibndg) == 0 else np.mean(icohp_mean_orb_antibndg)
+ )
+ df.loc[ids, "Icohp_antibndg_orb_mean_max"] = (
+ 0 if len(icohp_mean_orb_antibndg) == 0 else np.max(icohp_mean_orb_antibndg)
+ )
+ df.loc[ids, "Icohp_antibndg_orb_mean_min"] = (
+ 0 if len(icohp_mean_orb_antibndg) == 0 else np.min(icohp_mean_orb_antibndg)
+ )
+ df.loc[ids, "Icohp_antibndg_orb_mean_std"] = (
+ 0 if len(icohp_mean_orb_antibndg) == 0 else np.std(icohp_mean_orb_antibndg)
+ )
+
+ df.loc[ids, "Icohp_antibndg_orb_sum_avg"] = (
+ 0 if len(icohp_sum_orb_antibndg) == 0 else np.mean(icohp_sum_orb_antibndg)
+ )
+ df.loc[ids, "Icohp_antibndg_orb_sum_max"] = (
+ 0 if len(icohp_sum_orb_antibndg) == 0 else np.max(icohp_sum_orb_antibndg)
+ )
+ df.loc[ids, "Icohp_antibndg_orb_sum_min"] = (
+ 0 if len(icohp_sum_orb_antibndg) == 0 else np.min(icohp_sum_orb_antibndg)
+ )
+ df.loc[ids, "Icohp_antibndg_orb_sum_std"] = (
+ 0 if len(icohp_sum_orb_antibndg) == 0 else np.std(icohp_sum_orb_antibndg)
+ )
+
+ df.loc[ids, "antibonding_orb_perc_avg"] = 0 if len(antibond_orb) == 0 else np.mean(antibond_orb)
+ df.loc[ids, "antibonding_orb_perc_max"] = 0 if len(antibond_orb) == 0 else np.max(antibond_orb)
+ df.loc[ids, "antibonding_orb_perc_min"] = 0 if len(antibond_orb) == 0 else np.min(antibond_orb)
+ df.loc[ids, "antibonding_orb_perc_std"] = 0 if len(antibond_orb) == 0 else np.std(antibond_orb)
+
+ # add madelung energies for the structure
+ df.loc[ids, "Madelung_Mull"] = data["madelung_energies"]["Mulliken"]
+ df.loc[ids, "Madelung_Loew"] = data["madelung_energies"]["Loewdin"]
+
+ return df
+
+
+
+[docs]
+ @staticmethod
+ def read_lobster_lightweight_json(path_to_json: str | Path) -> dict:
+ """
+ Read the lightweight JSON.gz files and return a Python dictionary object.
+
+ :param path_to_json: path to lobsterpy lightweight json file
+
+ Returns:
+ Returns a dictionary with lobster summarized bonding analysis data
+
+ """
+ with gzip.open(str(path_to_json), "rb") as f:
+ data = json.loads(f.read().decode("utf-8"))
+
+ lobster_data = {}
+ for item in data:
+ for key in item:
+ # check applicable only for updated cba jsons from atomate2
+ try:
+ if (key == "cation_anion_bonds" or key == "all_bonds") and (
+ "sites" in item[key]["lobsterpy_data"]["sites"]
+ ):
+ item[key]["lobsterpy_data"]["sites"] = item[key]["lobsterpy_data"]["sites"]["sites"]
+ except KeyError:
+ pass
+ lobster_data.update(item)
+
+ return lobster_data
+
+
+
+[docs]
+ @staticmethod
+ def get_lobsterpy_cba_dict(path_to_lobster_calc: str | Path, bonds: str, orbital_resolved: bool) -> dict:
+ """
+ Generate a Python dictionary object using the Analysis class with condensed bonding analysis data.
+
+ :param path_to_lobster_calc: path to lobsterpy lightweight json file
+ :param bonds: "all" or "cation-anion" bonds
+ :param orbital_resolved: bool indicating whether analysis is performed orbital wise
+
+ Returns:
+ Returns a dictionary with lobster summarized bonding analysis data
+
+ """
+ file_paths = get_file_paths(
+ path_to_lobster_calc=path_to_lobster_calc, requested_files=["structure", "cohpcar", "icohplist", "charge"]
+ )
+
+ which_bonds = bonds.replace("-", "_")
+ bond_type = f"{which_bonds}_bonds"
+
+ try:
+ analyse = Analysis(
+ path_to_poscar=str(file_paths.get("structure")),
+ path_to_icohplist=str(file_paths.get("icohplist")),
+ path_to_cohpcar=str(file_paths.get("cohpcar")),
+ path_to_charge=str(file_paths.get("charge")),
+ summed_spins=False, # we will always use spin polarization here
+ cutoff_icohp=0.10,
+ which_bonds=which_bonds,
+ orbital_resolved=orbital_resolved,
+ )
+
+ data = {bond_type: {"lobsterpy_data": analyse.condensed_bonding_analysis}}
+ except ValueError:
+ data = {bond_type: {"lobsterpy_data": {}}}
+
+ try:
+ madelung_path = get_file_paths(path_to_lobster_calc=path_to_lobster_calc, requested_files=["madelung"])
+ madelung_obj = MadelungEnergies(filename=str(madelung_path.get("madelung")))
+
+ madelung_energies = {
+ "Mulliken": madelung_obj.madelungenergies_Mulliken,
+ "Loewdin": madelung_obj.madelungenergies_Loewdin,
+ "Ewald_splitting": madelung_obj.ewald_splitting,
+ }
+ data["madelung_energies"] = madelung_energies
+ except Exception:
+ warnings.warn(
+ "MadelungEnergies.lobster file not found in Lobster calc directory provided"
+ " Will set Madelung Energies for crystal structure values to NaN",
+ stacklevel=2,
+ )
+ madelung_energies = {
+ "Mulliken": np.nan,
+ "Loewdin": np.nan,
+ "Ewald_splitting": np.nan,
+ }
+
+ data["madelung_energies"] = madelung_energies
+
+ return data
+
+
+
+
+
+[docs]
+class CoxxFingerprint(NamedTuple):
+ """
+ Represents a Coxx fingerprint.
+
+ This named tuple is used to store information related to a Coxx fingerprint, which
+ includes energies, Coxx values, fingerprint type, spin type, number of bins, and bin width.
+
+ :param energies: The energy values associated with the Coxx fingerprint.
+ :param coxx: The Coxx values corresponding to each energy.
+ :param fp_type: The type of the Coxx fingerprint.
+ :param spin_type: The spin type associated with the fingerprint.
+ :param n_bins: The number of bins used in the Coxx fingerprint.
+ :param bin_width: The width of each bin in the Coxx fingerprint.
+ """
+
+ energies: np.ndarray
+ coxx: np.ndarray
+ fp_type: str
+ spin_type: str
+ n_bins: int
+ bin_width: float
+
+
+
+
+[docs]
+class FeaturizeCOXX:
+ """
+ Class to featurize COHPCAR, COBICAR or COOPCAR data.
+
+ :param path_to_coxxcar: path to COXXCAR.lobster (e.g., `COXXCAR.lobster`)
+ :param path_to_icoxxlist: path to ICOXXLIST.lobster (e.g., `ICOXXLIST.lobster`)
+ :param path_to_structure: path to structure file (e.g., `POSCAR`)
+ :param feature_type: set the feature type for moment features and fingerprints.
+ Possible options are `bonding`, `antibonding` or `overall`.
+ :param are_cobis: bool indicating if file contains COBI/ICOBI data.
+ :param are_coops: bool indicating if file contains COOP/ICOOP data.
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ """
+
+ def __init__(
+ self,
+ path_to_coxxcar: str | Path,
+ path_to_icoxxlist: str | Path,
+ path_to_structure: str | Path,
+ feature_type: str,
+ e_range: list[float] = [-10.0, 0.0],
+ are_cobis: bool = False,
+ are_coops: bool = False,
+ ):
+ """
+ Featurize COHPCAR, COBICAR or COOPCAR data.
+
+ :param path_to_coxxcar: path to COXXCAR.lobster (e.g., `COXXCAR.lobster`)
+ :param path_to_icoxxlist: path to ICOXXLIST.lobster (e.g., `ICOXXLIST.lobster`)
+ :param path_to_structure: path to structure file (e.g., `POSCAR`)
+ :param feature_type: set the feature type for moment features and fingerprints.
+ Possible options are `bonding`, `antibonding` or `overall`.
+ :param are_cobis: bool indicating if file contains COBI/ICOBI data
+ :param are_coops: bool indicating if file contains COOP/ICOOP data
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+
+ """
+ self.path_to_coxxcar = path_to_coxxcar
+ self.path_to_icoxxlist = path_to_icoxxlist
+ self.path_to_structure = path_to_structure
+ self.feature_type = feature_type
+ self.e_range = e_range
+ self.are_cobis = are_cobis
+ self.are_coops = are_coops
+ self.icoxxlist = Icohplist(
+ filename=self.path_to_icoxxlist,
+ are_cobis=self.are_cobis,
+ are_coops=self.are_coops,
+ )
+ self.completecoxx = CompleteCohp.from_file(
+ fmt="LOBSTER",
+ filename=self.path_to_coxxcar,
+ structure_file=self.path_to_structure,
+ are_cobis=self.are_cobis,
+ are_coops=self.are_coops,
+ )
+
+
+[docs]
+ def get_coxx_fingerprint_df(
+ self,
+ ids: str | None = None,
+ label_list: list[str] | None = None,
+ orbital: str | None = None,
+ per_bond: bool = True,
+ spin_type: str = "summed",
+ binning: bool = True,
+ n_bins: int = 56,
+ normalize: bool = True,
+ ) -> pd.DataFrame:
+ """
+ Generate a COXX fingerprints dataframe.
+
+ :param ids: set index name in the pandas dataframe. Default is None.
+ When None, LOBSTER calc directory name is used as index name.
+ :param spin_type: Specify spin type. Can accept '{summed/up/down}'
+ (default is summed)
+ :param binning: If true coxxs will be binned
+ :param n_bins: Number of bins to be used in the fingerprint (default is 256)
+ :param normalize: If true, normalizes the area under fp to equal to 1 (default is True)
+ :param label_list: Specify bond labels as a list for which cohp fingerprints are needed
+ :param orbital: Orbital for which fingerprint needs is to be computed. Cannot be used independently.
+ Always a needs label_list.
+ :param per_bond: Will scale cohp values by number of bonds i.e. length of label_list arg
+ (Only affects when label_list is not None)
+
+ Raises:
+ Exception: If spin_type is not one of the accepted values {summed/up/down}.
+ ValueError: If feature_type is not one of the accepted values {bonding/antibonding/overall}.
+
+ Returns:
+ A pandas dataframe with the COXX fingerprint
+ of format (energies, coxx, fp_type, spin_type, n_bins, bin_width)
+
+ """
+ coxxcar_obj = self.completecoxx
+
+ energies = coxxcar_obj.energies - coxxcar_obj.efermi
+
+ min_e = self.e_range[0]
+ max_e = self.e_range[-1]
+
+ if max_e is None:
+ max_e = np.max(energies)
+
+ if min_e is None:
+ min_e = np.min(energies)
+
+ if label_list is not None:
+ divisor = len(label_list) if per_bond else 1
+ if label_list is not None and orbital is None:
+ coxxcar_obj = coxxcar_obj.get_summed_cohp_by_label_list(label_list, divisor=divisor).get_cohp()
+ elif label_list and orbital:
+ coxxcar_obj = coxxcar_obj.get_summed_cohp_by_label_and_orbital_list(
+ label_list=label_list,
+ orbital_list=[orbital] * len(label_list),
+ divisor=divisor,
+ ).get_cohp()
+ else:
+ coxxcar_obj = coxxcar_obj.get_cohp()
+
+ if spin_type == "up":
+ coxx_all = coxxcar_obj[Spin.up]
+ elif spin_type == "down":
+ if Spin.down in coxxcar_obj:
+ coxx_all = coxxcar_obj[Spin.down]
+ else:
+ raise ValueError("LOBSTER calculation is non-spin polarized. Please switch spin_type to `up`")
+ elif spin_type == "summed":
+ if Spin.down in coxxcar_obj:
+ coxx_all = coxxcar_obj[Spin.up] + coxxcar_obj[Spin.down]
+ else:
+ coxx_all = coxxcar_obj[Spin.up]
+ else:
+ raise Exception("Check the spin_type argument.Possible options are summed/up/down")
+
+ coxx_dict = {}
+ tol = 1e-6
+ if not self.are_cobis and not self.are_coops:
+ coxx_dict["bonding"] = np.array([scohp if scohp <= tol else 0 for scohp in coxx_all])
+ coxx_dict["antibonding"] = np.array([scohp if scohp >= tol else 0 for scohp in coxx_all])
+ else:
+ coxx_dict["antibonding"] = np.array([scohp if scohp <= tol else 0 for scohp in coxx_all])
+ coxx_dict["bonding"] = np.array([scohp if scohp >= tol else 0 for scohp in coxx_all])
+
+ coxx_dict["overall"] = coxx_all
+
+ try:
+ if ids:
+ df = pd.DataFrame(index=[ids], columns=["COXX_FP"])
+ else:
+ ids = Path(self.path_to_coxxcar).parent.name
+ df = pd.DataFrame(index=[ids], columns=["COXX_FP"])
+
+ coxxs = coxx_dict[self.feature_type]
+ if len(energies) < n_bins:
+ inds = np.where((energies >= min_e - tol) & (energies <= max_e + tol))
+ fp = CoxxFingerprint(
+ energies[inds],
+ coxxs[inds],
+ self.feature_type,
+ spin_type,
+ len(energies),
+ np.diff(energies)[0],
+ )
+
+ df.loc[ids, "COXX_FP"] = fp
+
+ return df
+
+ if binning:
+ ener_bounds = np.linspace(min_e, max_e, n_bins + 1)
+ ener = ener_bounds[:-1] + (ener_bounds[1] - ener_bounds[0]) / 2.0
+ bin_width = np.diff(ener)[0]
+ else:
+ ener_bounds = np.array(energies)
+ ener = np.append(energies, [energies[-1] + np.abs(energies[-1]) / 10])
+ n_bins = len(energies)
+ bin_width = np.diff(energies)[0]
+
+ coxx_rebin = np.zeros(ener.shape)
+
+ for ii, e1, e2 in zip(range(len(ener)), ener_bounds[0:-1], ener_bounds[1:]):
+ inds = np.where((energies >= e1) & (energies < e2))
+ coxx_rebin[ii] = np.sum(coxxs[inds])
+ if normalize: # scale DOS bins to make area under histogram equal 1
+ area = np.sum([abs(coxx) * bin_width for coxx in coxx_rebin])
+ coxx_rebin_sc = coxx_rebin / area
+ else:
+ coxx_rebin_sc = coxx_rebin
+
+ fp = CoxxFingerprint(
+ np.array([ener]),
+ coxx_rebin_sc,
+ self.feature_type,
+ spin_type,
+ n_bins,
+ bin_width,
+ )
+
+ df.loc[ids, "COXX_FP"] = fp
+
+ return df
+
+ except KeyError:
+ raise ValueError(
+ "Please recheck fingerprint type requested argument. Possible options are bonding/antibonding/overall"
+ )
+
+
+ def _calculate_wicoxx_ein(self) -> tuple[float, float, float, float]:
+ r"""
+ Calculate weighted ICOXX (xx ==hp,op,bi) and EIN of crystal structure based on work by Nelson et al.
+
+ More details can be found here: https://doi.org/10.1016/B978-0-12-823144-9.00120-5
+
+ \\[\bar{COHP}(E) = \\sum_{i} \\left( w_{i} \\cdot COHP_{i}(E) \right)\\]
+
+ where:
+ - \\(w_{i} = \frac{ICOHP_{i}}{ICOHP_{\text{total}}}\\)
+
+ \\[EIN = \frac{{ICOHP_{\text{{total}}}}}{{\\overline{ICOHP}}} \\cdot \frac{2}{{N_{\text{{Atoms}}}}}\\]
+
+ where:
+ - \\(\\overline{ICOHP}\\) represents the average ICOHP value.
+ - \\(ICOHP_{\text{{total}}}\\) is the total ICOHP value.
+ - \\(N_{\text{{Atoms}}}\\) is the number of atoms in the system.
+
+ Returns:
+ Percent bonding, Percent anti-bonding, weighted icoxx, effective interaction number
+
+ """
+ list_labels = list(self.icoxxlist.icohplist.keys())
+ # Compute sum of icohps
+ icoxx_total = self.icoxxlist.icohpcollection.get_summed_icohp_by_label_list(list_labels)
+
+ summed_weighted_coxx = []
+
+ for lab in list_labels:
+ for cohp in self.completecoxx.get_cohp_by_label(f"{lab}", summed_spin_channels=True).cohp.values():
+ coxx = cohp
+ icoxx = self.icoxxlist.icohpcollection.get_icohp_by_label(lab, summed_spin_channels=True)
+ # calculate the weights based on icohp contri to total icohp of the structure
+ try:
+ weight = icoxx / icoxx_total
+ except ZeroDivisionError:
+ weight = 0
+ weighted_coxx = weight * coxx
+ summed_weighted_coxx.append(weighted_coxx)
+
+ summed = np.sum(summed_weighted_coxx, axis=0)
+ # below fermi
+ indices = self.completecoxx.energies <= self.completecoxx.efermi
+ en_bf = self.completecoxx.energies[indices]
+ coxx_bf = summed[indices]
+
+ w_icoxx = trapezoid(coxx_bf, en_bf)
+
+ ein = (icoxx_total / w_icoxx) * (2 / self.completecoxx.structure.num_sites) # calc effective interaction number
+
+ # percent bonding-anitbonding
+ tol = 1e-6
+ if not self.icoxxlist.are_cobis and not self.icoxxlist.are_coops:
+ bonding_indices = coxx_bf <= tol
+ antibonding_indices = coxx_bf >= tol
+ bnd = abs(trapezoid(en_bf[bonding_indices], coxx_bf[bonding_indices]))
+ antibnd = abs(trapezoid(en_bf[antibonding_indices], coxx_bf[antibonding_indices]))
+ per_bnd = (bnd / (bnd + antibnd)) * 100
+ per_antibnd = (antibnd / (bnd + antibnd)) * 100
+
+ elif self.icoxxlist.are_cobis or self.icoxxlist.are_coops:
+ bonding_indices = coxx_bf >= tol
+ antibonding_indices = coxx_bf <= tol
+ bnd = abs(trapezoid(coxx_bf[bonding_indices], en_bf[bonding_indices]))
+ antibnd = abs(trapezoid(coxx_bf[antibonding_indices], en_bf[antibonding_indices]))
+ per_bnd = (bnd / (bnd + antibnd)) * 100
+ per_antibnd = (antibnd / (bnd + antibnd)) * 100
+
+ return per_bnd, per_antibnd, w_icoxx, ein
+
+ @staticmethod
+ def _calc_moment_features(
+ complete_coxx_obj: CompleteCohp,
+ feature_type: str,
+ e_range: list[float],
+ label_list: list[str] | None = None,
+ orbital: str | None = None,
+ per_bond: bool = True,
+ ) -> tuple[float, float, float, float, float]:
+ """
+ Calculate band center,width, skewness, and kurtosis of the COXX.
+
+ :param complete_coxx_obj: CompleteCohp object
+ :param feature_type: feature type for moment features calculation
+ :param e_range: range of energy relative to fermi for which moment features needs to be computed
+ :param label_list: List of bond labels
+ :param orbital: orbital for which moment features need to be calculated. Cannot be used independently.
+ Always needs a label_list.
+ :param per_bond: Will scale cohp values by number of bonds i.e. length of label_list arg
+ (Only affects when label_list is not None)
+
+ Returns:
+ coxx center, width, skewness, kurtosis, and edge in eV
+ """
+ if label_list is not None:
+ divisor = len(label_list) if per_bond else 1
+
+ if label_list and orbital is None:
+ coxxcar = complete_coxx_obj.get_summed_cohp_by_label_list(label_list, divisor=divisor).get_cohp()
+ elif label_list and orbital:
+ coxxcar = complete_coxx_obj.get_summed_cohp_by_label_and_orbital_list(
+ label_list=label_list,
+ orbital_list=[orbital] * len(label_list),
+ divisor=divisor,
+ summed_spin_channels=False,
+ ).get_cohp()
+ else:
+ coxxcar = complete_coxx_obj.get_cohp()
+
+ coxx_all = coxxcar[Spin.up] + coxxcar[Spin.down] if Spin.down in coxxcar else coxxcar[Spin.up]
+ energies = complete_coxx_obj.energies - complete_coxx_obj.efermi
+
+ coxx_dict = {}
+ tol = 1e-6
+ if not complete_coxx_obj.are_cobis and not complete_coxx_obj.are_coops:
+ coxx_dict["bonding"] = np.array([scohp if scohp <= tol else 0 for scohp in coxx_all])
+ coxx_dict["antibonding"] = np.array([scohp if scohp >= tol else 0 for scohp in coxx_all])
+ coxx_dict["overall"] = coxx_all
+ else:
+ coxx_dict["antibonding"] = np.array([scohp if scohp <= tol else 0 for scohp in coxx_all])
+ coxx_dict["bonding"] = np.array([scohp if scohp >= tol else 0 for scohp in coxx_all])
+ coxx_dict["overall"] = coxx_all
+
+ try:
+ coxx_center = FeaturizeCOXX.get_coxx_center(
+ coxx=coxx_dict[feature_type],
+ energies=energies,
+ e_range=e_range,
+ )
+ coxx_width = np.sqrt(
+ FeaturizeCOXX.get_n_moment(
+ n=2,
+ coxx=coxx_dict[feature_type],
+ energies=energies,
+ e_range=e_range,
+ )
+ )
+ coxx_skew = FeaturizeCOXX.get_n_moment(
+ n=3,
+ coxx=coxx_dict[feature_type],
+ energies=energies,
+ e_range=e_range,
+ ) / FeaturizeCOXX.get_n_moment(
+ n=2,
+ coxx=coxx_dict[feature_type],
+ energies=energies,
+ e_range=e_range,
+ ) ** (3 / 2)
+
+ coxx_kurt = (
+ FeaturizeCOXX.get_n_moment(
+ n=4,
+ coxx=coxx_dict[feature_type],
+ energies=energies,
+ e_range=e_range,
+ )
+ / FeaturizeCOXX.get_n_moment(
+ n=2,
+ coxx=coxx_dict[feature_type],
+ energies=energies,
+ e_range=e_range,
+ )
+ ** 2
+ )
+ coxx_edge = FeaturizeCOXX.get_cohp_edge(coxx=coxx_dict[feature_type], energies=energies, e_range=e_range)
+ except KeyError:
+ raise ValueError(
+ "Please recheck feature type requested argument. Possible options are bonding/antibonding/overall"
+ )
+
+ return coxx_center, coxx_width, coxx_skew, coxx_kurt, coxx_edge
+
+
+[docs]
+ @staticmethod
+ def get_coxx_center(
+ coxx: ndarray[np.floating],
+ energies: ndarray[np.floating],
+ e_range: list[float],
+ ) -> float:
+ """
+ Get the bandwidth, defined as the first moment of the COXX.
+
+ :param coxx: COXX array
+ :param energies: energies corresponding COXX
+ :param e_range: range of energy to compute coxx center
+
+ Returns:
+ coxx center in eV
+ """
+ return FeaturizeCOXX.get_n_moment(n=1, coxx=coxx, energies=energies, e_range=e_range, center=False)
+
+
+
+[docs]
+ @staticmethod
+ def get_n_moment(
+ n: float,
+ coxx: ndarray[np.floating],
+ energies: ndarray[np.floating],
+ e_range: list[float] | None,
+ center: bool = True,
+ ) -> float:
+ """
+ Get the nth moment of COXX.
+
+ :param n: The order for the moment
+ :param coxx: COXX array
+ :param energies: energies array
+ :param e_range: range of energy to compute nth moment
+ :param center: Take moments with respect to the COXX center
+ :param e_range: range of energy to compute nth moment
+
+ Returns:
+ COXX nth moment in eV
+ """
+ if e_range:
+ min_e = e_range[0]
+ max_e = e_range[1]
+ if min_e is None:
+ min_e = min(energies)
+ if max_e is None:
+ max_e = max(energies)
+ else:
+ min_e = min(energies)
+ max_e = max(energies)
+
+ tol = 1e-6
+
+ mask = (energies >= min_e - tol) & (energies <= max_e + tol)
+
+ coxx = coxx[mask]
+ energies = energies[mask]
+
+ if center:
+ coxx_center = FeaturizeCOXX.get_coxx_center(coxx=coxx, energies=energies, e_range=[min_e, max_e])
+ p = energies - coxx_center
+ else:
+ p = energies
+
+ return np.trapz(p**n * coxx, x=energies) / np.trapz(coxx, x=energies)
+
+
+
+[docs]
+ @staticmethod
+ def get_cohp_edge(
+ coxx: ndarray[np.floating],
+ energies: ndarray[np.floating],
+ e_range: list[float] | None,
+ ):
+ """
+ Get the highest peak position of hilbert transformed COXX.
+
+ :param coxx: COXX array
+ :param energies: energies array
+ :param e_range: range of energy to coxx edge (max peak position)
+
+ Returns:
+ COXX edge (max peak position) in eV
+ """
+ if e_range:
+ min_e = e_range[0]
+ max_e = e_range[1]
+ if min_e is None:
+ min_e = min(energies)
+ if max_e is None:
+ max_e = max(energies)
+ else:
+ min_e = min(energies)
+ max_e = max(energies)
+
+ tol = 1e-6
+
+ mask = (energies >= min_e - tol) & (energies <= max_e + tol)
+
+ coxx = coxx[mask]
+ energies = energies[mask]
+
+ coxx_transformed = np.imag(hilbert(coxx))
+
+ return energies[np.argmax(coxx_transformed)]
+
+
+
+[docs]
+ def get_summarized_coxx_df(
+ self,
+ ids: str | None = None,
+ label_list: list[str] | None = None,
+ per_bond: bool = True,
+ ) -> pd.DataFrame:
+ """
+ Get a pandas dataframe with COXX features.
+
+ Features consist of weighted ICOXX, effective interaction number and
+ moment features of COXX in the selected energy range.
+
+ :param ids: set index name in the pandas dataframe. Default is None.
+ When None, LOBSTER calc directory name is used as index name.
+ :param label_list: list of bond labels to be used for generating features from
+ COHPCAR.lobster or COOPCAR.lobster or COBICAR.lobster. Default is None.
+ When None, all bond labels are used.
+ :param per_bond: Defaults to True. When True, features are normalized
+ by total number of bonds in the structure.
+
+ Returns:
+ Returns a pandas dataframe with cohp/cobi/coop related features as per input file
+
+ """
+ if ids:
+ df = pd.DataFrame(index=[ids])
+ else:
+ ids = Path(self.path_to_coxxcar).parent.name
+ df = pd.DataFrame(index=[ids])
+
+ (
+ per_bnd_xx,
+ per_antibnd_xx,
+ w_icoxx,
+ ein_xx,
+ ) = self._calculate_wicoxx_ein()
+
+ if self.are_cobis:
+ type_pop = "COBI"
+ elif self.are_coops:
+ type_pop = "COOP"
+ else:
+ type_pop = "COHP"
+
+ cc, cw, cs, ck, ce = FeaturizeCOXX._calc_moment_features(
+ complete_coxx_obj=self.completecoxx,
+ label_list=label_list,
+ per_bond=per_bond,
+ feature_type=self.feature_type,
+ e_range=self.e_range,
+ orbital=None,
+ )
+ df.loc[ids, f"bnd_wI{type_pop}"] = per_bnd_xx
+ df.loc[ids, f"antibnd_wI{type_pop}"] = per_antibnd_xx
+ df.loc[ids, f"w_I{type_pop}"] = w_icoxx
+ df.loc[ids, f"EIN_I{type_pop}"] = ein_xx
+ df.loc[ids, f"center_{type_pop}"] = cc
+ df.loc[ids, f"width_{type_pop}"] = cw
+ df.loc[ids, f"skewness_{type_pop}"] = cs
+ df.loc[ids, f"kurtosis_{type_pop}"] = ck
+ df.loc[ids, f"edge_{type_pop}"] = ce
+
+ return df
+
+
+
+
+
+[docs]
+class FeaturizeCharges:
+ """
+ Class to compute ionicity from CHARGE.lobster data.
+
+ :param path_to_structure: path to POSCAR
+ :param path_to_charge: path to CHARGE.lobster (e.g., `CHARGE.lobster`)
+ :param charge_type: set charge type used for computing ionicity.
+ Possible options are `Mulliken` or `Loewdin`
+ """
+
+ def __init__(
+ self,
+ path_to_structure: str | Path,
+ path_to_charge: str | Path,
+ charge_type: str,
+ ):
+ """
+ Compute the Ionicity of the structure from CHARGE.lobster data.
+
+ :param path_to_structure: path to POSCAR
+ :param path_to_charge: path to CHARGE.lobster (e.g., `CHARGE.lobster`)
+ :param charge_type: set charge type used for computing ionicity.
+ Possible options are `Mulliken` or `Loewdin`
+ """
+ self.path_to_structure = path_to_structure
+ self.path_to_charge = path_to_charge
+ self.charge_type = charge_type
+
+ def _calc_ionicity(self) -> float:
+ r"""
+ Calculate ionicity of the crystal structure based on quantum chemical charges.
+
+ \\[I_{\text{{Charges}}} = \frac{1}{{N_{\text{{Atoms}}}}}\\sum_{i=1}^{N_{\text{{Atoms}}} }
+ \\left(\frac{q_i}{v_{\text{{eff}},i}}\right)\\]
+
+ Returns:
+ Ionicity of the structure
+
+ """
+ chargeobj = Charge(filename=self.path_to_charge)
+ structure = Structure.from_file(self.path_to_structure)
+
+ if self.charge_type.lower() not in ["mulliken", "loewdin"]:
+ raise ValueError("Please check the requested charge_type. Possible options are `mulliken` or `loewdin`")
+
+ ch_veff = []
+ tol = 1e-6
+ for i, j in enumerate(getattr(chargeobj, self.charge_type.capitalize())):
+ if (
+ j > tol
+ and not structure.species[i].is_transition_metal
+ and (not structure.species[i].is_actinoid and not structure.species[i].is_lanthanoid)
+ ):
+ valence_elec = element(structure.species[i].value)
+ try:
+ val = j / valence_elec.nvalence()
+ except ZeroDivisionError:
+ val = 0
+ ch_veff.append(val)
+
+ elif (
+ j < tol
+ and not structure.species[i].is_transition_metal
+ and (not structure.species[i].is_actinoid and not structure.species[i].is_lanthanoid)
+ ):
+ valence_elec = element(structure.species[i].value)
+ val = j / (valence_elec.nvalence() - 8) if valence_elec.nvalence() != 8 else 0
+ ch_veff.append(val)
+
+ elif j > tol and (
+ structure.species[i].is_transition_metal
+ or structure.species[i].is_actinoid
+ or structure.species[i].is_lanthanoid
+ ):
+ val = (
+ j / (structure.species[i].max_oxidation_state - 0)
+ if structure.species[i].max_oxidation_state != tol
+ else 0
+ )
+ ch_veff.append(val)
+
+ elif j < tol and (
+ structure.species[i].is_transition_metal
+ or structure.species[i].is_actinoid
+ or structure.species[i].is_lanthanoid
+ ):
+ val = (
+ j / (structure.species[i].max_oxidation_state - 8)
+ if structure.species[i].max_oxidation_state != 8
+ else 0
+ )
+ ch_veff.append(val)
+
+ return sum(ch_veff) / structure.num_sites
+
+
+[docs]
+ def get_df(self, ids: str | None = None) -> pd.DataFrame:
+ """
+ Return a pandas dataframe with computed ionicity as columns.
+
+ :param ids: set index name in the pandas dataframe. Default is None.
+ When None, LOBSTER calc directory name is used as index name.
+
+ Returns:
+ Returns a pandas dataframe with ionicity
+
+ """
+ if ids:
+ df = pd.DataFrame(index=[ids])
+ else:
+ ids = Path(self.path_to_charge).parent.name
+ df = pd.DataFrame(index=[ids])
+
+ if self.charge_type.lower() == "mulliken":
+ df.loc[ids, "Ionicity_Mull"] = self._calc_ionicity()
+ else:
+ df.loc[ids, "Ionicity_Loew"] = self._calc_ionicity()
+
+ return df
+
+
+
+
+
+[docs]
+class DosFingerprint(NamedTuple):
+ """
+ Represents a Density of States (DOS) fingerprint.
+
+ This named tuple is used to store information related to the Density of States (DOS)
+ in a material. It includes the energies, densities, type, number of bins, and bin width.
+
+ :param energies: The energy values associated with the DOS.
+ :param densities: The corresponding density values for each energy.
+ :param type: The type of DOS fingerprint.
+ :param n_bins: The number of bins used in the fingerprint.
+ :param bin_width: The width of each bin in the DOS fingerprint.
+ """
+
+ energies: np.ndarray
+ densities: np.ndarray
+ type: str
+ n_bins: int
+ bin_width: float
+
+
+
+
+[docs]
+class FeaturizeDoscar:
+ """
+ Class to compute DOS moments and fingerprints from DOSCAR.lobster / DOSCAR.LSO.lobster.
+
+ :param path_to_structure: path to POSCAR
+ :param path_to_doscar: path to DOSCAR.lobster or DOSCAR.LSO.lobster
+ :param e_range: range of energy relative to fermi for which moment features and
+ features needs to be computed
+ :param add_element_dos_moments: add element dos moment features alongside orbital dos
+ """
+
+ def __init__(
+ self,
+ path_to_structure: str | Path,
+ path_to_doscar: str | Path,
+ e_range: list[float] | None = [-10.0, 0.0],
+ add_element_dos_moments: bool = False,
+ ):
+ """
+ Featurize DOSCAR.lobster or DOSCAR.LSO.lobster data.
+
+ :param path_to_structure: path to POSCAR
+ :param path_to_doscar: path to DOSCAR.lobster or DOSCAR.LSO.lobster
+ :param e_range: range of energy relative to fermi for which moment features and
+ features needs to be computed
+ :param add_element_dos_moments: add element dos moment features alongside orbital dos
+ """
+ self.path_to_structure = path_to_structure
+ self.path_to_doscar = path_to_doscar
+ self.e_range = e_range
+ self.dos = Doscar(doscar=self.path_to_doscar, structure_file=self.path_to_structure).completedos
+ self.add_element_dos_moments = add_element_dos_moments
+
+
+[docs]
+ def get_df(self, ids: str | None = None) -> pd.DataFrame:
+ """
+ Return a pandas dataframe with computed DOS moment features as columns.
+
+ :param ids: set index name in the pandas dataframe. Default is None.
+ When None, LOBSTER calc directory name is used as index name.
+
+ Moment features are PDOS center, width, skewness, kurtosis and upper band edge.
+
+ Returns:
+ A pandas dataframe object
+ """
+ if ids:
+ df = pd.DataFrame(index=[ids])
+ else:
+ ids = Path(self.path_to_doscar).parent.name
+ df = pd.DataFrame(index=[ids])
+
+ spd_dos_lobster = self.dos.get_spd_dos()
+
+ for orbital in spd_dos_lobster:
+ df.loc[ids, f"{orbital.name}_band_center"] = round(
+ self.dos.get_band_center(band=orbital, erange=self.e_range), 4
+ )
+ df.loc[ids, f"{orbital.name}_band_width"] = round(
+ self.dos.get_band_width(band=orbital, erange=self.e_range), 4
+ )
+ df.loc[ids, f"{orbital.name}_band_skew"] = round(
+ self.dos.get_band_skewness(band=orbital, erange=self.e_range), 4
+ )
+ df.loc[ids, f"{orbital.name}_band_kurtosis"] = round(
+ self.dos.get_band_kurtosis(band=orbital, erange=self.e_range), 4
+ )
+ df.loc[ids, f"{orbital.name}_band_upperband_edge"] = round(
+ self.dos.get_upper_band_edge(band=orbital, erange=self.e_range), 4
+ )
+ if self.add_element_dos_moments:
+ elements_list = self.dos.structure.elements
+ for el in elements_list:
+ if orbital in self.dos.get_element_spd_dos(el=el.name):
+ df.loc[ids, f"{el.name}_{orbital.name}_band_center"] = round(
+ self.dos.get_band_center(band=orbital, elements=[el], erange=self.e_range),
+ 4,
+ )
+ df.loc[ids, f"{el.name}_{orbital.name}_band_width"] = round(
+ self.dos.get_band_width(band=orbital, elements=[el], erange=self.e_range),
+ 4,
+ )
+ df.loc[ids, f"{el.name}_{orbital.name}_band_skew"] = round(
+ self.dos.get_band_skewness(band=orbital, elements=[el], erange=self.e_range),
+ 4,
+ )
+ df.loc[ids, f"{el.name}_{orbital.name}_band_kurtosis"] = round(
+ self.dos.get_band_kurtosis(band=orbital, elements=[el], erange=self.e_range),
+ 4,
+ )
+ df.loc[ids, f"{el.name}_{orbital.name}_band_upperband_edge"] = round(
+ self.dos.get_upper_band_edge(band=orbital, elements=[el], erange=self.e_range),
+ 4,
+ )
+
+ return df
+
+
+
+[docs]
+ def get_fingerprint_df(
+ self,
+ ids: str | None = None,
+ fp_type: str = "summed_pdos",
+ binning: bool = True,
+ n_bins: int = 256,
+ normalize: bool = True,
+ ) -> pd.DataFrame:
+ """
+ Generate a dataframe consisting of DOS fingerprint (fp).
+
+ :param ids: set index name in the pandas dataframe. Default is None.
+ When None, LOBSTER calc directory name is used as index name.
+ :param fp_type: Specify fingerprint type to compute, can accept `s/p/d/f/summed_pdos`
+ (default is summed_pdos)
+ :param binning: If true, the DOS fingerprint is binned using np.linspace and n_bins.
+ Default is True.
+ :param n_bins: Number of bins to be used in the fingerprint (default is 256)
+ :param normalize: If true, normalizes the area under fp to equal to 1. Default is True.
+
+ Returns:
+ A pandas dataframe object with DOS fingerprints
+ """
+ if ids:
+ df = pd.DataFrame(index=[ids], columns=["DOS_FP"])
+ else:
+ ids = Path(self.path_to_doscar).parent.name
+ df = pd.DataFrame(index=[ids], columns=["DOS_FP"])
+
+ fp = self.dos.get_dos_fp(
+ type=fp_type,
+ normalize=normalize,
+ n_bins=n_bins,
+ binning=binning,
+ max_e=self.e_range[-1] if self.e_range is not None else None,
+ min_e=self.e_range[0] if self.e_range is not None else None,
+ )._asdict()
+
+ df.loc[ids, "DOS_FP"] = DosFingerprint(**fp)
+
+ return df
+
+
+
+# Copyright (c) lobsterpy development team
+# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License
+
+"""This package provides the modules for featurzing Lobster data ready for ML studies."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from monty.os.path import zpath
+
+
+
+[docs]
+def get_file_paths(
+ path_to_lobster_calc: str | Path = "", requested_files: list[str] = [], use_lso_dos: bool = True
+) -> dict:
+ """
+ Get file paths for LobsterPy featurizations, raise Exception if not all of requested paths exist.
+
+ :param path_to_lobster_calc: path to root LOBSTER calc directory
+ :param requested_files: files to return paths for.
+ :param use_lso_dos: solely required for BatchDosFeaturizer.
+ Will force featurizer to use DOSCAR.LSO.lobster instead of DOSCAR.lobster.
+
+ :return: dict that assigns each item of requested_files its path
+
+ """
+ default_values = {
+ "structure": "POSCAR",
+ "cohpcar": "COHPCAR.lobster",
+ "icohplist": "ICOHPLIST.lobster",
+ "cobicar": "COBICAR.lobster",
+ "icobilist": "ICOBILIST.lobster",
+ "coopcar": "COOPCAR.lobster",
+ "icooplist": "ICOOPLIST.lobster",
+ "charge": "CHARGE.lobster",
+ "madelung": "MadelungEnergies.lobster",
+ "doscar": ("DOSCAR.LSO.lobster" if use_lso_dos else "DOSCAR.lobster"),
+ "lobsterin": "lobsterin",
+ "lobsterout": "lobsterout",
+ "bandoverlaps": "bandOverlaps.lobster",
+ "potcar": "POTCAR",
+ "vasprun": "vasprun.xml",
+ "incar": "INCAR",
+ }
+
+ lobster_path = Path(path_to_lobster_calc)
+ file_paths = {}
+ missing_files = []
+
+ for file in requested_files:
+ file_str = default_values.get(file)
+ file_str = file_str if isinstance(file_str, str) else file
+ if file == "structure":
+ try:
+ file_paths[file] = get_structure_path(lobster_path=lobster_path)
+ except Exception:
+ missing_files.append(default_values["structure"])
+ else:
+ file_path = lobster_path / file_str
+ if file_path.exists():
+ file_paths[file] = file_path
+ else:
+ gz_file_path = Path(zpath(file_path))
+ if gz_file_path.exists():
+ file_paths[file] = gz_file_path
+ else:
+ missing_files.append(default_values[file])
+
+ if missing_files:
+ raise Exception(f"Files {missing_files} not found in {lobster_path.name}.")
+
+ return file_paths
+
+
+
+
+[docs]
+def get_structure_path(lobster_path: Path) -> Path:
+ """
+ Search iteratively for (unzipped / zipped) structure file.
+
+ POSCAR is prioritized over POSCAR.lobster.
+
+ :param lobster_path: path to root LOBSTER calc directory
+
+ :return: path to structure file
+ """
+ for filename in ["POSCAR", "POSCAR.lobster"]:
+ poscar_path = lobster_path / filename
+ if poscar_path.exists():
+ return poscar_path
+ gz_file_path = Path(zpath(poscar_path))
+ if gz_file_path.exists():
+ return gz_file_path
+
+ raise Exception
+
+
+# Copyright (c) lobsterpy development team
+# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License
+
+"""Here classes and functions to plot Lobster outputs are provided."""
+
+from __future__ import annotations
+
+import typing
+from itertools import cycle
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+import matplotlib as mpl
+import numpy as np
+import plotly.graph_objs as go
+from matplotlib import pyplot as plt
+from pymatgen.core import Structure
+from pymatgen.electronic_structure.cohp import Cohp, CompleteCohp, IcohpCollection
+from pymatgen.electronic_structure.core import Spin
+from pymatgen.electronic_structure.dos import LobsterCompleteDos
+from pymatgen.electronic_structure.plotter import CohpPlotter, DosPlotter
+
+if TYPE_CHECKING:
+ from lobsterpy.cohp.analyze import Analysis
+from lobsterpy.plotting import layout_dicts as ld
+
+
+
+[docs]
+def get_style_list(
+ no_base_style: bool = False,
+ styles: list[str | dict[str, Any]] | None = None,
+ **kwargs,
+) -> list[str | dict[str, Any]]:
+ """
+ Get args for matplotlib.style from user input.
+
+ Remaining kwargs are collected as a dict and take the highest priority.
+
+ :param no_base_style: If true, do not include lobsterpy_base.mplstyle
+ :param styles: User-requested styles. These can be paths to mplstyle files,
+ the names of known (matplotlib-supplied) styles, or dicts of rcParam options.
+ :param kwargs: matplotlib-style sheet keyword arguments
+
+ """
+ base = [] if no_base_style else [str(Path(__file__).absolute().parent / "lobsterpy_base.mplstyle")]
+ if styles is None:
+ styles = []
+
+ return base + styles + [kwargs]
+
+
+
+
+[docs]
+class PlainCohpPlotter(CohpPlotter):
+ """
+ Modified Pymatgen CohpPlotter with styling removed.
+
+ This allows the styling to be manipulated more easily using matplotlib
+ style sheets.
+
+ :param zero_at_efermi: Shift all populations to have zero
+ energy at the Fermi level. Defaults to True.
+ :param are_coops: Bool indicating that populations are COOPs, not COHPs.
+ Defaults to False for COHPs.
+ :param are_cobis: Bool indicating that populations are COBIs, not COHPs.
+ Defaults to False for COHPs.
+ """
+
+
+[docs]
+ def get_plot(
+ self,
+ ax: mpl.axes.Axes | None = None,
+ xlim: tuple[float, float] | None = None,
+ ylim: tuple[float, float] | None = None,
+ plot_negative: bool | None = None,
+ integrated: bool = False,
+ invert_axes: bool = True,
+ sigma: float | None = None,
+ ):
+ """
+ Get a matplotlib plot showing the COHP.
+
+ :param ax: Existing Matplotlib Axes object to plot to.
+ :param xlim: Specifies the x-axis limits. Defaults to None for
+ automatic determination.
+ :param ylim: Specifies the y-axis limits. Defaults to None for
+ automatic determination.
+ :param plot_negative: It is common to plot -COHP(E) so that the
+ sign means the same for COOPs and COHPs. Defaults to None
+ for automatic determination: If are_coops is True, this
+ will be set to False, else it will be set to True.
+ :param integrated: Switch to plot ICOHPs. Defaults to False.
+ :param invert_axes: Put the energies onto the y-axis, which is
+ common in chemistry.
+ :param sigma: Standard deviation of Gaussian broadening applied to
+ population data. If this is unset (None) no broadening will be
+ added.
+
+ Returns:
+ A matplotlib object.
+
+ """
+ if self.are_coops and not self.are_cobis:
+ cohp_label = "COOP"
+ elif self.are_cobis and not self.are_coops:
+ cohp_label = "COBI"
+ elif self.are_cobis and self.are_coops:
+ raise ValueError("Plot data should not contain COBI and COOP data at same time")
+ else:
+ cohp_label = "COHP" + " (eV)"
+
+ if plot_negative is None:
+ plot_negative = (not self.are_coops) and (not self.are_cobis)
+
+ if integrated:
+ cohp_label = "I" + cohp_label
+
+ if plot_negative:
+ cohp_label = "$-$" + cohp_label
+
+ energy_label = "$E - E_f$ (eV)" if self.zero_at_efermi else "$E$ (eV)"
+
+ colors = mpl.rcParams["axes.prop_cycle"].by_key()["color"]
+ ncolors = len(colors)
+
+ if ax is None:
+ _, ax = plt.subplots()
+
+ allpts = []
+ keys = self._cohps.keys()
+ for i, key in enumerate(keys):
+ energies = self._cohps[key]["energies"]
+ populations = self._cohps[key]["COHP"] if not integrated else self._cohps[key]["ICOHP"]
+ for spin in [Spin.up, Spin.down]:
+ if spin in populations:
+ if invert_axes:
+ x = -populations[spin] if plot_negative else populations[spin]
+ y = energies
+ x = self._broaden(y, x, sigma=sigma)
+ else:
+ x = energies
+ y = -populations[spin] if plot_negative else populations[spin]
+ y = self._broaden(x, y, sigma=sigma)
+ allpts.extend(list(zip(x, y)))
+ if spin == Spin.up:
+ ax.plot(
+ x,
+ y,
+ color=colors[i % ncolors],
+ linestyle="-",
+ label=str(key),
+ )
+ else:
+ ax.plot(x, y, color=colors[i % ncolors], linestyle="--")
+
+ if xlim:
+ ax.set_xlim(xlim)
+ xlim = ax.get_xlim()
+ assert isinstance(xlim, tuple)
+
+ if ylim:
+ ax.set_ylim(ylim)
+ else:
+ relevanty = [p[1] for p in allpts if xlim[0] < p[0] < xlim[1]]
+ if relevanty:
+ plt.ylim((min(relevanty), max(relevanty)))
+
+ grid_like_line_kwargs = {
+ "color": mpl.rcParams["grid.color"],
+ "linewidth": mpl.rcParams["grid.linewidth"],
+ "linestyle": mpl.rcParams["grid.linestyle"],
+ "alpha": mpl.rcParams["grid.alpha"],
+ "zorder": 0,
+ }
+
+ if not invert_axes:
+ ax.axhline(**grid_like_line_kwargs)
+
+ if self.zero_at_efermi:
+ ax.axvline(**grid_like_line_kwargs)
+
+ else:
+ ax.axvline(self._cohps[key]["efermi"], **grid_like_line_kwargs)
+ else:
+ ax.axvline(**grid_like_line_kwargs)
+
+ if self.zero_at_efermi:
+ ax.axhline(**grid_like_line_kwargs)
+ else:
+ ax.axhline(self._cohps[key]["efermi"], **grid_like_line_kwargs)
+
+ if invert_axes:
+ plt.xlabel(cohp_label)
+ plt.ylabel(energy_label)
+ else:
+ plt.xlabel(energy_label)
+ plt.ylabel(cohp_label)
+
+ _ = ax.legend()
+
+ return plt
+
+
+ @staticmethod
+ def _broaden(energies: np.ndarray, population: np.ndarray, sigma=None, cutoff=4.0):
+ """
+ Broaden the spectrum with a given standard deviation.
+
+ The population is convolved with a normalised Gaussian kernel. This
+ requires the energy grid to be regularly-spaced.
+
+ :param energies: Regularly-spaced energy series
+ :param population: Population data for broadening
+ :param sigma: Standard deviation for Gaussian broadening. If sigma is None
+ then the input data is returned without any processing.
+ :param cutoff: Range cutoff for broadening kernel, as a multiple of sigma.
+
+ Return:
+ Broadened population
+
+ """
+ from scipy.signal import convolve
+ from scipy.stats import norm
+
+ if sigma is None:
+ return population
+
+ spacing = np.mean(np.diff(energies))
+ if not np.allclose(np.diff(energies), spacing, atol=1e-5):
+ raise ValueError("Energy grid is not regular, cannot broaden with discrete convolution.")
+
+ # Obtain symmetric mesh for broadening kernel, centered on zero
+ kernel_x = np.arange(0, cutoff * sigma + 0.5 * spacing, spacing)
+ kernel_x = np.concatenate([-kernel_x[-1:1:-1], kernel_x])
+
+ kernel = norm.pdf(kernel_x, scale=sigma)
+
+ return convolve(population, kernel, mode="same") / kernel.sum()
+
+
+
+
+[docs]
+class PlainDosPlotter(DosPlotter):
+ """
+ Modified Pymatgen DosPlotter with styling removed.
+
+ This allows the styling to be manipulated more easily using matplotlib
+ style sheets. It also adds additional functionalities to plotter
+
+ :param zero_at_efermi: Shift all DOS to have zero
+ energy at the Fermi level. Defaults to True.
+ :param stack: Bool indicating that plot should be stacked
+ area graph.
+ :param sigma: Standard deviation for gaussian smearing.
+ :param summed: Will plot summed dos spin populations.
+ Defaults to False.
+ """
+
+ def __init__(
+ self,
+ zero_at_efermi: bool = True,
+ stack: bool = False,
+ summed: bool = False,
+ sigma: float | None = None,
+ ) -> None:
+ """
+ Initialize DOS plotter.
+
+ :param zero_at_efermi: Whether to shift all Dos to have zero energy at the
+ fermi energy. Defaults to True.
+ :param stack: Whether to plot the DOS as a stacked area graph
+ :param summed: Whether to plot the summed spins DOS.
+ :param sigma: Specify a standard deviation for Gaussian smearing
+ the DOS for nicer looking plots. Defaults to None for no smearing.
+
+ """
+ super().__init__(zero_at_efermi, stack, sigma)
+ self.zero_at_efermi = zero_at_efermi
+ self.stack = stack
+ self.sigma = sigma
+ self.summed = summed
+ self._norm_val = True
+ self._doses = {} # type: ignore
+
+
+[docs]
+ def add_dos(self, label: str, dos: LobsterCompleteDos) -> None:
+ """
+ Add a dos for plotting.
+
+ :param label: label for the DOS. Must be unique.
+ :param dos: LobsterCompleteDos object
+
+ """
+ if dos.norm_vol is None:
+ self._norm_val = False
+ energies = dos.energies
+ if self.summed:
+ if self.sigma:
+ smeared_densities = dos.get_smeared_densities(self.sigma)
+ if Spin.down in smeared_densities:
+ added_densities = smeared_densities[Spin.up] + smeared_densities[Spin.down]
+ densities = {Spin.up: added_densities}
+ else:
+ densities = smeared_densities
+ else:
+ densities = {Spin.up: dos.get_densities()}
+ else:
+ densities = dos.get_smeared_densities(self.sigma) if self.sigma else dos.densities
+
+ efermi = dos.efermi
+
+ self._doses[label] = {
+ "energies": energies,
+ "densities": densities,
+ "efermi": efermi,
+ }
+
+
+
+[docs]
+ def add_site_orbital_dos(self, dos: LobsterCompleteDos, orbital: str, site_index: int):
+ """
+ Add orbital dos at particular site.
+
+ :param dos: LobsterCompleteDos object
+ :param orbital: Orbitals name at the site. Must be unique.
+ :param site_index: site index in the structure
+
+ """
+ if dos.norm_vol is None:
+ self._norm_val = False
+ site = dos.structure.sites[site_index]
+ avail_orbs = list(dos.pdos[site])
+ if orbital not in avail_orbs and orbital != "all":
+ str_orbs = ", ".join(avail_orbs)
+ raise ValueError(f"Requested orbital is not available for this site, available orbitals are {str_orbs}")
+
+ if orbital == "all":
+ for orb in avail_orbs:
+ dos_obj = dos.get_site_orbital_dos(site=site, orbital=orb)
+ label = site.species_string + str(site_index + 1) + f": {orb}"
+ energies = dos_obj.energies
+ if self.summed:
+ if self.sigma:
+ smeared_densities = dos_obj.get_smeared_densities(self.sigma)
+ if Spin.down in smeared_densities:
+ added_densities = smeared_densities[Spin.up] + smeared_densities[Spin.down]
+ densities = {Spin.up: added_densities}
+ else:
+ densities = smeared_densities
+ else:
+ densities = {Spin.up: dos_obj.get_densities()}
+ else:
+ densities = dos_obj.get_smeared_densities(self.sigma) if self.sigma else dos_obj.densities
+
+ efermi = dos_obj.efermi
+
+ self._doses[label] = {
+ "energies": energies,
+ "densities": densities,
+ "efermi": efermi,
+ }
+ else:
+ dos_obj = dos.get_site_orbital_dos(site=site, orbital=orbital)
+ label = site.species_string + str(site_index + 1) + f": {orbital}"
+
+ energies = dos_obj.energies
+ if self.summed:
+ if self.sigma:
+ smeared_densities = dos_obj.get_smeared_densities(self.sigma)
+ if Spin.down in smeared_densities:
+ added_densities = smeared_densities[Spin.up] + smeared_densities[Spin.down]
+ densities = {Spin.up: added_densities}
+ else:
+ densities = smeared_densities
+ else:
+ densities = {Spin.up: dos_obj.get_densities()}
+ else:
+ densities = dos_obj.get_smeared_densities(self.sigma) if self.sigma else dos_obj.densities
+
+ efermi = dos_obj.efermi
+
+ self._doses[label] = {
+ "energies": energies,
+ "densities": densities,
+ "efermi": efermi,
+ }
+
+
+
+[docs]
+ @typing.no_type_check
+ def get_plot(
+ self,
+ ax: mpl.axes.Axes | None = None,
+ xlim: tuple[float, float] | None = None,
+ ylim: tuple[float, float] | None = None,
+ invert_axes: bool = False,
+ beta_dashed: bool = False,
+ sigma: float | None = None,
+ ):
+ """
+ Get a matplotlib plot showing the COHP.
+
+ :param ax: Existing Matplotlib Axes object to plot to.
+ :param xlim: Specifies the x-axis limits. Defaults to None for
+ automatic determination.
+ :param ylim: Specifies the y-axis limits. Defaults to None for
+ automatic determination.
+ :param invert_axes: Put the energies onto the y-axis, which is
+ common in chemistry.
+ :param beta_dashed: Plots the beta spin channel with a dashed line. Defaults to False
+ :param sigma: Standard deviation of Gaussian broadening applied to population data.
+ If this is unset (None) no broadening will be added.
+
+ Returns:
+ A matplotlib object.
+
+ """
+ ys = None
+ all_densities = []
+ all_energies = []
+
+ colors = mpl.rcParams["axes.prop_cycle"].by_key()["color"]
+ n_colors = len(colors)
+
+ if ax is None:
+ _, ax = plt.subplots()
+
+ # Note that this complicated processing of energies is to allow for
+ # stacked plots in matplotlib.
+ for dos in self._doses.values():
+ energies = dos["energies"]
+ densities = dos["densities"]
+ if not ys:
+ ys = {
+ Spin.up: np.zeros(energies.shape),
+ Spin.down: np.zeros(energies.shape),
+ }
+ new_dens = {}
+ for spin in [Spin.up, Spin.down]:
+ if spin in densities:
+ if self.stack:
+ ys[spin] += densities[spin]
+ new_dens[spin] = ys[spin].copy()
+ else:
+ new_dens[spin] = densities[spin]
+ all_energies.append(energies)
+ all_densities.append(new_dens)
+
+ keys = list(reversed(self._doses))
+ all_densities.reverse()
+ all_energies.reverse()
+ all_pts = []
+
+ for idx, key in enumerate(keys):
+ for spin in [Spin.up, Spin.down]:
+ if spin in all_densities[idx]:
+ energy = all_energies[idx]
+ densities = list(int(spin) * all_densities[idx][spin])
+ if invert_axes:
+ x = densities
+ y = energy
+ else:
+ x = energy
+ y = densities
+ all_pts.extend(list(zip(x, y)))
+ if self.stack:
+ ax.fill(x, y, color=colors[idx % n_colors], label=str(key))
+ elif spin == Spin.down and beta_dashed:
+ ax.plot(
+ x,
+ y,
+ color=colors[idx % n_colors],
+ label=str(key),
+ linestyle="--",
+ linewidth=3,
+ )
+ else:
+ ax.plot(
+ x,
+ y,
+ color=colors[idx % n_colors],
+ label=str(key),
+ linewidth=3,
+ )
+
+ if xlim:
+ ax.set_xlim(xlim)
+ if ylim:
+ ax.set_ylim(ylim)
+ elif not invert_axes:
+ xlim = ax.get_xlim()
+ relevant_y = [p[1] for p in all_pts if xlim[0] < p[0] < xlim[1]]
+ ax.set_ylim((min(relevant_y), max(relevant_y)))
+ if not xlim and invert_axes:
+ ylim = ax.get_ylim()
+ relevant_y = [p[0] for p in all_pts if ylim[0] < p[1] < ylim[1]]
+ ax.set_xlim((min(relevant_y), max(relevant_y)))
+
+ if self.zero_at_efermi:
+ xlim = ax.get_xlim()
+ ylim = ax.get_ylim()
+ (ax.plot(xlim, [0, 0], "k--", linewidth=2) if invert_axes else ax.plot([0, 0], ylim, "k--", linewidth=2))
+
+ if invert_axes:
+ ax.set_ylabel("Energies (eV)")
+ ax.set_xlabel(f"Density of states (states/eV{'/ų' if self._norm_val else ''})")
+ ax.axvline(x=0, color="k", linestyle="--", linewidth=2)
+ else:
+ ax.set_xlabel("Energies (eV)")
+ if self._norm_val:
+ ax.set_ylabel("Density of states (states/eV/ų)")
+ else:
+ ax.set_ylabel("Density of states (states/eV)")
+ ax.axhline(y=0, color="k", linestyle="--", linewidth=2)
+
+ # Remove duplicate labels with a dictionary
+ handles, labels = ax.get_legend_handles_labels()
+ label_dict = dict(zip(labels, handles))
+ ax.legend(label_dict.values(), label_dict.keys())
+ legend_text = ax.get_legend().get_texts() # all the text.Text instance in the legend
+ plt.setp(legend_text, fontsize=30)
+ plt.tight_layout()
+ _ = ax.legend()
+
+ return plt
+
+
+
+
+
+[docs]
+class InteractiveCohpPlotter(CohpPlotter):
+ """Interactive COHP, COBI or COOP plotter to view all relevant bonds in one figure.
+
+ :param zero_at_efermi: Shift all populations to have zero
+ energy at the Fermi level. Defaults to True.
+ :param are_coops: Bool indicating that populations are COOPs, not COHPs.
+ Defaults to False for COHPs.
+ :param are_cobis: Bool indicating that populations are COBIs, not COHPs.
+ Defaults to False for COHPs.
+ """
+
+ COLOR_PALETTE = [
+ "#e41a1c",
+ "#377eb8",
+ "#4daf4a",
+ "#984ea3",
+ "#ff7f00",
+ "#ffff33",
+ "#a65628",
+ "#f781bf",
+ "#999999",
+ ]
+
+
+[docs]
+ def add_cohp(self, label: str, cohp: Cohp):
+ """
+ Add COHP object to the plotter.
+
+ :param label: Label for the COHP. Must be unique.
+ :param cohp: COHP object.
+
+ """
+ if "All" not in self._cohps:
+ self._cohps["All"] = {}
+
+ energies = cohp.energies - cohp.efermi if self.zero_at_efermi else cohp.energies
+
+ if label not in self._cohps["All"]:
+ self._cohps["All"].update(
+ {
+ label: {
+ "energies": energies,
+ "COHP": cohp.get_cohp(),
+ "ICOHP": cohp.get_icohp(),
+ "efermi": cohp.efermi,
+ }
+ }
+ )
+ else:
+ raise ValueError(
+ "Please use another label to add the COHP, provided label already exists "
+ "in the plot data, which will result in overwriting the existing COHP data."
+ )
+
+
+
+[docs]
+ def add_all_relevant_cohps(
+ self,
+ analyse: Analysis,
+ suffix: str = "",
+ label_resolved: bool = True,
+ orbital_resolved: bool = False,
+ ) -> None:
+ """
+ Add all relevant COHPs from lobsterpy analyse object.
+
+ :param analyse: Analyse object from lobsterpy.
+ :param suffix: Optional addition to LOBSTER label to avoid key conflicts when plotting multiple
+ calcs or just for additional legend information.
+ :param label_resolved: bool indicating to obtain label resolved interactive plots for relevant bonds.
+ If false, will return summed cohp curves of unique relevant bonds.
+ :param orbital_resolved: bool indicating to include orbital resolved interactive cohps for relevant bonds.
+
+ """
+ complete_cohp = analyse.chemenv.completecohp
+
+ plot_data = analyse.get_site_bond_resolved_labels()
+
+ if "All" not in self._cohps:
+ self._cohps["All"] = {}
+
+ # iterate and extract the data to be plotted from cohp objects
+ for bond_key, labels in plot_data.items():
+ count = len(labels)
+ label_with_count = self._insert_number_of_bonds_in_label(
+ label=bond_key, character=":", number_of_bonds=count
+ )
+ # get summed cohps from the relevant bond label at the site
+ cohp = complete_cohp.get_summed_cohp_by_label_list(label_list=labels)
+ energies = cohp.energies - cohp.efermi if self.zero_at_efermi else cohp.energies
+ drop_down_key = plot_legend = label_with_count + suffix # label for the dropdown menu
+ self._cohps["All"].update(
+ {
+ plot_legend: {
+ "energies": energies,
+ "COHP": cohp.get_cohp(),
+ "ICOHP": cohp.get_icohp(),
+ "efermi": cohp.efermi,
+ }
+ }
+ )
+ if len(plot_data) > 1:
+ if drop_down_key not in self._cohps:
+ self._cohps[drop_down_key] = {}
+ self._cohps[drop_down_key].update(
+ {
+ plot_legend: {
+ "energies": energies,
+ "COHP": cohp.get_cohp(),
+ "ICOHP": cohp.get_icohp(),
+ "efermi": cohp.efermi,
+ }
+ }
+ )
+
+ # Add cohp data for each relevant bond label iteratively
+ if label_resolved and not orbital_resolved:
+ if label_with_count + suffix not in self._cohps:
+ self._cohps[label_with_count + suffix] = {}
+ # Get cohp data for each relevant bond label at the site
+ for label in labels:
+ cohp = complete_cohp.get_cohp_by_label(label)
+ energies = cohp.energies - cohp.efermi if self.zero_at_efermi else cohp.energies
+ drop_down_key = label_with_count + suffix # label for the dropdown menu
+ plot_legend = self._get_plot_label_for_label_resolved(
+ structure=analyse.structure,
+ label_list=[label],
+ complete_cohp=complete_cohp,
+ orb_list=[],
+ label_resolved=True,
+ orbital_resolved=False,
+ )
+ if len(plot_data) > 1:
+ self._cohps[drop_down_key].update(
+ {
+ plot_legend: {
+ "energies": energies,
+ "COHP": cohp.get_cohp(),
+ "ICOHP": cohp.get_icohp(),
+ "efermi": cohp.efermi,
+ }
+ }
+ )
+
+ plot_legend_here = plot_legend + suffix
+ self._cohps["All"].update(
+ {
+ plot_legend_here: {
+ "energies": energies,
+ "COHP": cohp.get_cohp(),
+ "ICOHP": cohp.get_icohp(),
+ "efermi": cohp.efermi,
+ }
+ }
+ )
+ # Adds cohp data for each relevant orbitals and bond label iteratively
+ if orbital_resolved and label_resolved:
+ if label_with_count + suffix not in self._cohps:
+ self._cohps[label_with_count + suffix] = {}
+ # get relevant orbitals associated with each bond label
+ plot_data_orb = analyse.get_site_orbital_resolved_labels()
+ drop_down_key = label_with_count + suffix # label for the dropdown menu
+ key_val = plot_data_orb[bond_key]
+ # get cohp data for each orbital and associated bond labels iteratively
+ for orb, val in key_val.items():
+ for lab in val["bond_labels"]:
+ mapped_bond_labels = [item for item in [lab] for _ in range(len(val["relevant_sub_orbitals"]))]
+
+ cohp_orb = complete_cohp.get_summed_cohp_by_label_and_orbital_list(
+ label_list=mapped_bond_labels, orbital_list=val["relevant_sub_orbitals"]
+ )
+
+ energies = cohp_orb.energies - cohp_orb.efermi if self.zero_at_efermi else cohp_orb.energies
+ # plot legends will contain species and orbital along with relevant bond label
+ plot_legend = self._get_plot_label_for_label_resolved(
+ structure=analyse.structure,
+ label_list=[lab],
+ complete_cohp=complete_cohp,
+ orb_list=[orb],
+ orbital_resolved=True,
+ label_resolved=True,
+ )
+ if len(plot_data) > 1:
+ self._cohps[drop_down_key].update(
+ {
+ plot_legend: {
+ "energies": energies,
+ "COHP": cohp_orb.get_cohp(),
+ "ICOHP": cohp_orb.get_icohp(),
+ "efermi": cohp_orb.efermi,
+ }
+ }
+ )
+
+ plot_legend_here = plot_legend + suffix
+
+ self._cohps["All"].update(
+ {
+ plot_legend_here: {
+ "energies": energies,
+ "COHP": cohp_orb.get_cohp(),
+ "ICOHP": cohp_orb.get_icohp(),
+ "efermi": cohp_orb.efermi,
+ }
+ }
+ )
+ # Adds summed cohp data for each relevant orbitals
+ if orbital_resolved and not label_resolved:
+ if label_with_count + suffix not in self._cohps:
+ self._cohps[label_with_count + suffix] = {}
+ # get relevant orbitals associated with each bond label
+ plot_data_orb = analyse.get_site_orbital_resolved_labels()
+ drop_down_key = label_with_count + suffix # label for the dropdown menu
+ key_val = plot_data_orb[bond_key]
+ # get summed cohp data for each relevant orbital
+ for orb, val in key_val.items():
+ mapped_bond_labels = [
+ item for item in val["bond_labels"] for _ in range(len(val["relevant_sub_orbitals"]))
+ ]
+ cohp_orb = complete_cohp.get_summed_cohp_by_label_and_orbital_list(
+ label_list=mapped_bond_labels,
+ orbital_list=val["relevant_sub_orbitals"] * len(val["bond_labels"]),
+ )
+
+ energies = cohp_orb.energies - cohp_orb.efermi if self.zero_at_efermi else cohp_orb.energies
+ # plot legends will contain species and orbital along with number of bonds at the site
+ plot_legend = self._get_plot_label_for_label_resolved(
+ structure=analyse.structure,
+ label_list=val["bond_labels"],
+ complete_cohp=complete_cohp,
+ orb_list=[orb],
+ orbital_resolved=True,
+ label_resolved=False,
+ )
+
+ if len(plot_data) > 1:
+ self._cohps[drop_down_key].update(
+ {
+ plot_legend: {
+ "energies": energies,
+ "COHP": cohp_orb.get_cohp(),
+ "ICOHP": cohp_orb.get_icohp(),
+ "efermi": cohp_orb.efermi,
+ }
+ }
+ )
+
+ plot_legend_here = plot_legend + suffix
+
+ self._cohps["All"].update(
+ {
+ plot_legend_here: {
+ "energies": energies,
+ "COHP": cohp_orb.get_cohp(),
+ "ICOHP": cohp_orb.get_icohp(),
+ "efermi": cohp_orb.efermi,
+ }
+ }
+ )
+
+
+
+[docs]
+ def add_cohps_by_lobster_label(self, analyse: Analysis, label_list: list[str], suffix: str = ""):
+ """
+ Add COHPs explicitly specified in label list.
+
+ :param analyse: Analyse object from lobsterpy.
+ :param label_list: List of COHP labels as from LOBSTER.
+ :param suffix: Optional addition to LOBSTER label to avoid key
+ conflicts when plotting multiple calcs or just for additional legend information.
+
+ """
+ complete_cohp = analyse.chemenv.completecohp
+
+ if "All" not in self._cohps:
+ self._cohps["All"] = {}
+
+ for label in label_list:
+ atom1 = complete_cohp.bonds[label]["sites"][0].species_string
+ atom2 = complete_cohp.bonds[label]["sites"][1].species_string
+ sorted_label = sorted([atom1, atom2])
+ cohp = complete_cohp.get_cohp_by_label(label)
+ energies = cohp.energies - cohp.efermi if self.zero_at_efermi else cohp.energies
+ key = sorted_label[0] + "-" + sorted_label[1] + ": " + label + suffix
+ self._cohps["All"].update(
+ {
+ key: {
+ "energies": energies,
+ "COHP": cohp.get_cohp(),
+ "ICOHP": cohp.get_icohp(),
+ "efermi": cohp.efermi,
+ }
+ }
+ )
+
+
+
+[docs]
+ def add_cohps_from_plot_data(self, plot_data_dict: dict[str, Cohp], suffix: str = ""):
+ """
+ Add all relevant COHPs for specified bond type from lobster lightweight json.gz file.
+
+ :param plot_data_dict: Lobsterpy plot data dict
+ :param suffix: Optional addition to LOBSTER label to avoid key
+ conflicts when plotting multiple calcs or just for additional legend information.
+
+ """
+ # convert to cohp objects
+ plot_data_dict = plot_data_dict.copy()
+ for key, cohps in plot_data_dict.items():
+ if isinstance(cohps, Cohp):
+ plot_data_dict.update({key: cohps})
+ else:
+ try:
+ cohps = Cohp.from_dict(cohps)
+ plot_data_dict.update({key: cohps})
+ except (TypeError, AttributeError):
+ raise ValueError(
+ "The data provided could not be converted to cohp object.Please recheck the input data"
+ )
+
+ if "All" not in self._cohps:
+ self._cohps["All"] = {}
+
+ for bond_key, cohps in plot_data_dict.items():
+ energies = cohps.energies - cohps.efermi if self.zero_at_efermi else cohps.energies
+ key = bond_key + suffix
+ self._cohps["All"].update(
+ {
+ key: {
+ "energies": energies,
+ "COHP": cohps.get_cohp(),
+ "ICOHP": cohps.get_icohp(),
+ "efermi": cohps.efermi,
+ }
+ }
+ )
+
+
+
+[docs]
+ def get_plot(
+ self,
+ xlim: list[float] | None = None,
+ rangeslider: bool = False,
+ ylim: list[float] | None = None,
+ plot_negative: bool | None = None,
+ integrated: bool = False,
+ invert_axes: bool = True,
+ sigma: float | None = None,
+ colors: list[str] | None = None,
+ ):
+ """
+ Get an interactive plotly figure showing the COHPs.
+
+ :param xlim: Specifies the x-axis limits. Defaults to None for
+ automatic determination.
+ :param rangeslider: Adds a plotly.graph_objs.layout.xaxis.Rangeslider
+ object to figure to allow easy manipulation of x-axis..
+ :param ylim: Specifies the y-axis limits. Defaults to None for
+ automatic determination.
+ :param plot_negative: It is common to plot -COHP(E) so that the
+ sign means the same for COOPs and COHPs. Defaults to None
+ for automatic determination - If are_coops is True, this
+ will be set to False, else it will be set to True.
+ :param integrated: Switch to plot ICOHPs. Defaults to False.
+ :param invert_axes: Put the energies onto the y-axis, which is
+ common in chemistry.
+ :param sigma: Standard deviation of Gaussian broadening applied to
+ population data. If this is unset (None) no broadening will be added.
+ :param colors: list of hex color codes to be used in plot
+
+ Returns:
+ A plotly.graph_objects.Figure object.
+
+ """
+ if self.are_coops and not self.are_cobis:
+ cohp_label = "COOP"
+ elif self.are_cobis and not self.are_coops:
+ cohp_label = "COBI"
+ elif self.are_cobis and self.are_coops:
+ raise ValueError("Plot data should not contain COBI and COOP data at same time")
+ else:
+ cohp_label = "COHP" + " (eV)"
+
+ if plot_negative is None:
+ plot_negative = (not self.are_coops) and (not self.are_cobis)
+
+ if integrated:
+ cohp_label = "I" + cohp_label
+
+ if plot_negative:
+ cohp_label = "\u2212" + cohp_label
+
+ energy_label = "$E - E_f \\text{ (eV)}$" if self.zero_at_efermi else "$E \\text{ (eV)}$"
+
+ # Setting up repeating color scheme (same as for matplotlib plots in .mplstyle)
+ palette = InteractiveCohpPlotter.COLOR_PALETTE if colors is None else colors
+ pal_iter = cycle(palette)
+
+ traces = {} # type: ignore
+ for k, v in self._cohps.items():
+ traces.update({k: []})
+ for label in v:
+ population_key = v[label]["ICOHP"] if integrated else v[label]["COHP"]
+ band_color = next(pal_iter)
+ for spin in [Spin.up, Spin.down]:
+ if spin in population_key:
+ population = [-i for i in population_key[spin]] if plot_negative else population_key[spin]
+ if invert_axes:
+ x = population
+ y = v[label]["energies"]
+ x = PlainCohpPlotter._broaden(y, x, sigma=sigma)
+ else:
+ x = v[label]["energies"]
+ y = population
+ y = PlainCohpPlotter._broaden(x, y, sigma=sigma)
+ if spin == Spin.up:
+ trace = go.Scatter(x=x, y=y, name=label)
+ trace.update(ld.spin_up_trace_style_dict)
+ else:
+ trace = go.Scatter(x=x, y=y, name="")
+ trace.update(ld.spin_down_trace_style_dict)
+ trace.update(line={"color": band_color})
+ traces[k].append(trace)
+
+ energy_axis = (
+ go.layout.YAxis(title=energy_label)
+ if invert_axes
+ else go.layout.XAxis(title=energy_label, rangeslider={"visible": rangeslider})
+ )
+ energy_axis.update(ld.energy_axis_style_dict)
+ cohp_axis = (
+ go.layout.XAxis(title=cohp_label, rangeslider={"visible": rangeslider})
+ if invert_axes
+ else go.layout.YAxis(title=cohp_label)
+ )
+ cohp_axis.update(ld.cohp_axis_style_dict)
+
+ layout = (
+ go.Layout(xaxis=cohp_axis, yaxis=energy_axis)
+ if invert_axes
+ else go.Layout(xaxis=energy_axis, yaxis=cohp_axis)
+ )
+
+ # Create figure object
+ fig = go.Figure(layout=layout)
+ fig.update_layout(ld.layout_dict)
+ fig.update_layout(legend=ld.legend_style_dict)
+
+ # Add all traces to figure
+ for val_trace in traces.values():
+ for trace in val_trace:
+ fig.add_trace(trace)
+
+ # Update layout with dropdown menu if label resolved plot
+ if len(traces) > 2:
+ # Update visibility of traces
+ for i, _ in enumerate(fig.data):
+ if i <= len(traces["All"]) - 1:
+ pass
+ else:
+ fig.data[i].visible = False
+ # Add dropdown buttons
+ fig.update_layout(
+ updatemenus=[
+ {
+ "buttons": [
+ {
+ "args": [
+ {
+ "visible": [
+ selected_group == group
+ for group, val_trace in traces.items()
+ for _trace in val_trace
+ ]
+ }
+ ],
+ "label": selected_group,
+ "method": "update",
+ }
+ for selected_group in traces
+ ],
+ "direction": "down",
+ "showactive": True,
+ "active": 0,
+ "x": 0.5,
+ "y": 1.15,
+ "bgcolor": "rgba(255,255,255,0.8)",
+ "bordercolor": "rgba(0,0,0,0.2)",
+ "xanchor": "center",
+ "yanchor": "top",
+ "font": {"family": "Arial", "color": "#444444", "size": 18},
+ }
+ ]
+ )
+
+ if xlim:
+ fig.update_xaxes(range=xlim)
+ if ylim:
+ fig.update_yaxes(range=ylim)
+
+ fig.update_yaxes(automargin=True)
+
+ return fig
+
+
+ @staticmethod
+ def _insert_number_of_bonds_in_label(label: str, character: str, number_of_bonds: int) -> str:
+ """
+ Add number of bonds to bond label.
+
+ For example : for input label 'Ba1: Ba-Ti', character ':', number_of_bonds: 3,
+ Will return 'Ba1: 3 x Ba-Ti'
+
+ :param label: bond label to which number of bonds needs to be inserted
+ :param character: string character where number of bonds needs to be inserted
+ :param number_of_bonds: number of bonds corresponding to the label
+
+ Returns:
+ bond label with number of bonds inserted
+ """
+ return label.replace(character, f"{character} {number_of_bonds} x", 1)
+
+ @staticmethod
+ def _get_plot_label_for_label_resolved(
+ structure: Structure,
+ label_list: list[str],
+ complete_cohp: CompleteCohp,
+ orb_list: list[str],
+ label_resolved: bool = False,
+ orbital_resolved: bool = False,
+ ) -> str:
+ """
+ Generate plot labels for both orbital and label-resolved plots.
+
+ For example for NaCl structure, label:21, orb: 3s-3s
+ Will return '21: Cl2-Na1 (2.85 Å)' if label_resolved is True and orbital_resolved is False
+ Will return '21: Cl2(3s)-Na1(3s) (2.85 Å)' If label and orbital resolved True
+ Will return 'Cl(3s)-Na(3s) (2.85 Å)' if orbital_resolved is True and label_resolved is False
+
+ :param structure: pymatgen structure object
+ :param label_list: bond label to which number of bonds needs to be inserted
+ :param complete_cohp: complete cohp object
+ :param orb_list: relevant orbital
+ :param label_resolved: specifies type of label to be returned is for label_resolved case
+ :param orbital_resolved: specifies type of label to be returned is for orbital_resolved case
+
+ Returns:
+ plot label string
+ """
+ if label_resolved and not orbital_resolved:
+ atom_pairs = []
+ for site in complete_cohp.bonds[label_list[0]]["sites"]:
+ atom = f"{site.species_string}{structure.sites.index(site) + 1!s}"
+ atom_pairs.append(atom)
+ atom_pair_str = "-".join(atom_pairs)
+ bond_length = round(complete_cohp.bonds[label_list[0]]["length"], 2)
+ plot_label = f"{label_list[0]}: {atom_pair_str} ({bond_length} \u00c5)"
+
+ elif not label_resolved and orbital_resolved:
+ orb_atom_pairs = []
+ orb_pair = orb_list[0].split("-")
+ for site, site_orb in zip(complete_cohp.bonds[label_list[0]]["sites"], orb_pair):
+ atom_orb = f"{site.species_string} ({site_orb})"
+ orb_atom_pairs.append(atom_orb)
+ orb_atom_pairs_str = "-".join(orb_atom_pairs)
+ bond_length = round(complete_cohp.bonds[label_list[0]]["length"], 2)
+ plot_label = f"{len(label_list)}x {orb_atom_pairs_str} ({bond_length} \u00c5)"
+ else:
+ orb_atom_pairs = []
+ orb_pair = orb_list[0].split("-")
+ for site, site_orb in zip(complete_cohp.bonds[label_list[0]]["sites"], orb_pair):
+ atom_orb = f"{site.species_string}{structure.sites.index(site) + 1!s} ({site_orb})"
+ orb_atom_pairs.append(atom_orb)
+ orb_atom_pairs_str = "-".join(orb_atom_pairs)
+ bond_length = round(complete_cohp.bonds[label_list[0]]["length"], 2)
+ plot_label = f"{label_list[0]}: {orb_atom_pairs_str} ({bond_length} \u00c5)"
+
+ return plot_label
+
+
+
+
+[docs]
+class IcohpDistancePlotter:
+ """
+ Plotter to generate ICOHP or ICOBI or ICOOP vs bond lengths plots.
+
+ :param are_coops: Bool indicating that populations are ICOOPs, not ICOHPs.
+ Defaults to False for COHPs.
+ :param are_cobis: Bool indicating that populations are ICOBIs, not ICOHPs.
+ Defaults to False for COHPs.
+ """
+
+ def __init__(self, are_coops: bool = False, are_cobis: bool = False):
+ """Initialize ICOHPs or ICOBI or ICOOP vs bond lengths plotter."""
+ self.are_coops = are_coops
+ self.are_cobis = are_cobis
+ self._icohps = {} # type: ignore
+
+
+[docs]
+ def add_icohps(self, label: str, icohpcollection: IcohpCollection):
+ """
+ Add ICOHPs or ICOBIs or ICOOPS for plotting.
+
+ :param label: Label for the ICOHPs. Must be unique.
+ :param icohpcollection: IcohpCollection object.
+
+ """
+ icohps = []
+ bond_len = []
+ atom_pairs = []
+ orb_data = {} # type: ignore
+ for indx, bond_label in enumerate(icohpcollection._list_labels):
+ orb_data.update({bond_label: {}})
+ for k, v in icohpcollection._list_orb_icohp[indx].items():
+ orb_data[bond_label].update({k: sum(v["icohp"].values())})
+ icohps.append(sum(icohpcollection._list_icohp[indx].values()))
+ bond_len.append(icohpcollection._list_length[indx])
+ atom1 = icohpcollection._list_atom1[indx]
+ atom2 = icohpcollection._list_atom2[indx]
+ atom_pairs.append(atom1 + "-" + atom2)
+
+ self._icohps[label] = {
+ "atom_pairs": atom_pairs,
+ "bond_labels": icohpcollection._list_labels,
+ "icohps": icohps,
+ "bond_lengths": bond_len,
+ "orb_data": orb_data,
+ }
+
+
+
+[docs]
+ def get_plot(
+ self,
+ ax: mpl.axes.Axes | None = None,
+ marker_size: float = 50,
+ marker_style: str = "o",
+ xlim: tuple[float, float] | None = None,
+ ylim: tuple[float, float] | None = None,
+ plot_negative: bool = True,
+ ):
+ """
+ Get a matplotlib plot showing the COHP or COBI or COOP with respect to bond lengths.
+
+ :param ax: Existing Matplotlib Axes object to plot to.
+ :param marker_size: sets the size of markers in scatter plots
+ :param marker_style: sets type of marker used in plot
+ :param xlim: Specifies the x-axis limits. Defaults to None for
+ automatic determination.
+ :param ylim: Specifies the y-axis limits. Defaults to None for
+ automatic determination.
+ :param plot_negative: Will plot -1*ICOHPs. Works only for ICOHPs
+
+ Returns:
+ A matplotlib object.
+ """
+ if self.are_coops and not self.are_cobis:
+ cohp_label = "ICOOP"
+ elif self.are_cobis and not self.are_coops:
+ cohp_label = "ICOBI"
+ elif self.are_cobis and self.are_coops:
+ raise ValueError("Plot data should not contain ICOBI and ICOOP data at same time")
+ else:
+ cohp_label = "ICOHP (eV)"
+
+ if ax is None:
+ _, ax = plt.subplots()
+
+ if xlim:
+ ax.set_xlim(xlim)
+ if ylim:
+ ax.set_ylim(ylim)
+
+ ax.set_ylabel(cohp_label)
+ ax.set_xlabel("Bond lengths (\u00c5)")
+
+ for label, data in self._icohps.items():
+ x = data["bond_lengths"]
+ if plot_negative and cohp_label == "ICOHP (eV)":
+ ax.set_ylabel("$-$" + cohp_label)
+ y = [-1 * icohp for icohp in data["icohps"]]
+ else:
+ y = data["icohps"]
+
+ ax.scatter(x, y, s=marker_size, marker=marker_style, label=label)
+
+ return plt
+
+
+
+# Copyright (c) lobsterpy development team
+# Distributed under the terms of a BSD 3-Clause "New" or "Revised" License
+
+"""This module provides a class for generating graph objects using lobsterpy data."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+from pymatgen.core.structure import Structure
+from pymatgen.io.lobster.lobsterenv import LobsterNeighbors
+from pymatgen.io.lobster.outputs import Charge
+
+from lobsterpy.cohp.analyze import Analysis
+
+
+
+[docs]
+class LobsterGraph:
+ """
+ Class to generate structure graph objects with bonding data from Lobster.
+
+ Mode of automatic bonding analysis can be `cation-anion` or `all` bonds. The strongest bond is
+ determined based on the ICOHPs. The coordination environments are determined based on
+ `cutoff_icohp * ICOHPs` values. If the path of ICOBILIST (ICOOPLIST) is provided, the ICOBI (ICOOP)
+ values corresponding to relevant bond labels obtained from the ICOHPLIST are also added as edge properties
+ to the structure graph objects. The Mulliken and Loewdin charges are added as node properties to
+ the structure graph objects.
+
+ Attributes:
+ sg: return structure_graph object
+
+ :param path_to_poscar: path to POSCAR (e.g., "POSCAR").
+ :param path_to_charge: path to CHARGE.lobster (e.g., "CHARGE.lobster").
+ :param path_to_cohpcar: path to COHPCAR.lobster (e.g., "COHPCAR.lobster").
+ :param path_to_icohplist: path to ICOHPLIST.lobster (e.g., "ICOHPLIST.lobster").
+ :param path_to_icooplist: path to ICOOPLIST.lobster (e.g., "ICOOPLIST.lobster").
+ :param path_to_icobilist: path to ICOBILIST.lobster (e.g., "ICOBILIST.lobster").
+ :param path_to_madelung: path to MadelungEnergies.lobster (e.g., "MadelungEnergies.lobster")
+ :param cutoff_icohp: only bonds that are stronger than cutoff_icohp * strongest ICOHP will be considered.
+ :param noise_cutoff: if provided hardcodes the lower limit of icohps considered.
+ :param add_additional_data_sg: if True will add the information from ICOOPLIST.lobster
+ and ICOBILIST.lobster based on ICOHPLIST.lobster relevant bond.
+ :param which_bonds: selects which kind of bonds are analyzed. "all" is the default.
+ :param start: start energy for bonding antibonding percent integration.
+ """
+
+ def __init__(
+ self,
+ path_to_poscar: str | Path,
+ path_to_charge: str | Path,
+ path_to_cohpcar: str | Path,
+ path_to_icohplist: str | Path,
+ path_to_madelung: str | Path,
+ add_additional_data_sg: bool = True,
+ path_to_icooplist: str | Path | None = None,
+ path_to_icobilist: str | Path | None = None,
+ which_bonds: str = "all",
+ cutoff_icohp: float = 0.10,
+ noise_cutoff: float = 0.1,
+ start: float | None = None,
+ ):
+ """Initialize and return a structure graph object."""
+ if add_additional_data_sg:
+ self.add_additional_data_sg = add_additional_data_sg
+ if path_to_icooplist is not None and path_to_icobilist is not None:
+ self.path_to_icooplist = path_to_icooplist
+ self.path_to_icobilist = path_to_icobilist
+ else:
+ raise ValueError(
+ "add_additional_data_sg is set to True.Please provide path_to_icooplist and path_to_icobilist"
+ )
+ else:
+ self.add_additional_data_sg = add_additional_data_sg # type: ignore
+
+ self.path_to_poscar = path_to_poscar
+ self.path_to_charge = path_to_charge
+ self.path_to_cohpcar = path_to_cohpcar
+ self.path_to_icohplist = path_to_icohplist
+ self.path_to_madelung = path_to_madelung
+ self.which_bonds = which_bonds
+ self.cutoff_icohp = cutoff_icohp
+ self.noise_cutoff = noise_cutoff
+
+ if self.which_bonds == "all":
+ self.additional_condition = 0
+ elif self.which_bonds == "cation-anion":
+ self.additional_condition = 1
+ else:
+ raise ValueError(
+ "Only accepted values are 'all' and 'cation-anion'."
+ "Please check the input parameters of which_bonds arg"
+ )
+ self.start = start
+
+ self.sg = self.get_decorated_sg()
+
+
+[docs]
+ def get_decorated_sg(self):
+ """
+ Create a graph object decorated with bonding data from LobsterPy.
+
+ Returns:
+ structure graph object
+ """
+ if self.add_additional_data_sg:
+ chemenvlobster = LobsterNeighbors(
+ are_coops=False,
+ filename_icohp=self.path_to_icohplist,
+ perc_strength_icohp=self.cutoff_icohp,
+ structure=Structure.from_file(self.path_to_poscar),
+ additional_condition=self.additional_condition,
+ filename_charge=self.path_to_charge,
+ add_additional_data_sg=self.add_additional_data_sg,
+ filename_blist_sg1=self.path_to_icobilist,
+ id_blist_sg1="ICOBI",
+ filename_blist_sg2=self.path_to_icooplist,
+ id_blist_sg2="ICOOP",
+ valences_from_charges=True,
+ adapt_extremum_to_add_cond=True,
+ noise_cutoff=self.noise_cutoff,
+ )
+
+ else:
+ chemenvlobster = LobsterNeighbors(
+ are_coops=False,
+ filename_icohp=self.path_to_icohplist,
+ perc_strength_icohp=self.cutoff_icohp,
+ structure=Structure.from_file(self.path_to_poscar),
+ additional_condition=self.additional_condition,
+ filename_charge=self.path_to_charge,
+ add_additional_data_sg=self.add_additional_data_sg,
+ valences_from_charges=True,
+ adapt_extremum_to_add_cond=True,
+ noise_cutoff=self.noise_cutoff,
+ )
+
+ # Adds Mulliken and Löwdin charges as site properties to structure object (node properties)
+ decorated_structure = Charge(self.path_to_charge).get_structure_with_charges(self.path_to_poscar)
+
+ # Create the structure graph object decorated with site and edge properties based on ICOHP/ICOBI/ICOOP data
+ lobster_env = chemenvlobster.get_bonded_structure(
+ structure=decorated_structure, decorate=True, edge_properties=True
+ )
+
+ # Initialize automating bonding analysis from Lobsterpy based on ICOHP
+ analyze = Analysis(
+ path_to_charge=self.path_to_charge,
+ path_to_cohpcar=self.path_to_cohpcar,
+ path_to_poscar=self.path_to_poscar,
+ path_to_icohplist=self.path_to_icohplist,
+ path_to_madelung=self.path_to_madelung,
+ which_bonds=self.which_bonds,
+ cutoff_icohp=self.cutoff_icohp,
+ )
+
+ # Store the summarized dictionary object containing bonding information
+ cba = analyze.condensed_bonding_analysis
+
+ # Iterate over sites in the dictionary
+ for cba_data in cba["sites"].values():
+ for _, node_data in lobster_env.graph.nodes.data():
+ # Check if ions are same and add its corresponding environment in node properties
+ if cba_data["ion"] == node_data["specie"]:
+ node_data["properties"].update({"env": cba_data["env"]})
+
+ for edge_prop in lobster_env.graph.edges.data(): # Iterate over structure graph edges
+ _ab, ab_p, _b, b_p = analyze._integrate_antbdstates_below_efermi(
+ cohp=analyze.chemenv.completecohp.get_cohp_by_label(edge_prop[2]["bond_label"]),
+ start=self.start,
+ ) # Compute bonding- antibonding percentages for each bond in structure graph object
+ edge_prop[2]["ICOHP_bonding_perc"] = b_p # Store bonding percentage in edge of graph object
+ edge_prop[2]["ICOHP_antibonding_perc"] = ab_p # Store anti-bonding percentage in edge graph object
+
+ return lobster_env
+
+
+
Short
+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 00000000..2ea7ff3e --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = `` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = `` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 00000000..dbe1aaad --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/custom.css b/_static/custom.css new file mode 100644 index 00000000..e1fad460 --- /dev/null +++ b/_static/custom.css @@ -0,0 +1,128 @@ +/******************************************************************************* +* light theme +* +* all the variables used for light theme coloring +*/ +html[data-theme="light"] { + /***************************************************************************** + * main colors + */ + --pst-color-primary: rgb(82, 165, 171); + --pst-color-secondary: rgb(200, 30, 109); + --pst-color-success: rgb(40, 167, 69); + --pst-color-info: var(--pst-color-primary); + --pst-color-warning: var(--pst-color-secondary); + --pst-color-danger: rgb(220, 53, 69); + --pst-color-text-base: rgb(51, 51, 51); + --pst-color-text-muted: rgb(77, 77, 77); + --pst-color-border: rgb(201, 201, 201); + --pst-color-shadow: rgb(216, 216, 216); + + /***************************************************************************** + * depth colors + * + * background: the more in depth color + * on-background: the object directly set on the background, use of shadows in light theme + * surface: object set on the background (without shadows) + * on_surface: object set on surface object (without shadows) + */ + --pst-color-background: rgb(255, 255, 255); + --pst-color-on-background: rgb(242, 239, 233); + --pst-color-surface: rgb(242, 239, 233); + --pst-color-on-surface: rgb(242, 239, 233); + + /***************************************************************************** + * extentions + */ + + --pst-color-panel-background: var(--pst-color-background); + + /***************************************************************************** + * layout + */ + + // links + --pst-color-link: var(--pst-color-primary); + --pst-color-link-hover: var(--pst-color-secondary); + + // inline code + --pst-color-inline-code: rgb(232, 62, 140); + + // targeted content + --pst-color-target: rgb(251, 229, 78); + + // hide any content that should not be displayed in the light theme + .only-dark { + display: none !important; + } +} + +/******************************************************************************* +* dark theme +* +* all the variables used for dark theme coloring +*/ +html[data-theme="dark"] { + /***************************************************************************** + * main colors + */ + --pst-color-primary: rgb(82, 165, 171); + --pst-color-secondary: rgb(200, 30, 109); + --pst-color-success: rgb(72, 135, 87); + --pst-color-info: var(--pst-color-primary); + --pst-color-warning: var(--pst-color-secondary); + --pst-color-danger: rgb(203, 70, 83); + --pst-color-text-base: rgb(201, 209, 217); + --pst-color-text-muted: rgb(192, 192, 192); + --pst-color-border: rgb(192, 192, 192); + --pst-color-shadow: var(--pst-color-background); + + /***************************************************************************** + * depth colors + * + * background: the more in depth color + * on-background: the object directly set on the background, use of a light grey in dark theme + * surface: object set on the background (without shadows) + * on_surface: object set on surface object (without shadows) + */ + --pst-color-background: rgb(18, 18, 18); + --pst-color-on-background: rgb(30, 30, 30); + --pst-color-surface: rgb(41, 41, 41); + --pst-color-on-surface: rgb(55, 55, 55); + + /***************************************************************************** + * extentions + */ + + --pst-color-panel-background: var(--pst-color-background-up); + + /***************************************************************************** + * layout + */ + + // links + --pst-color-link: var(--pst-color-primary); + --pst-color-link-hover: var(--pst-color-secondary); + + // inline code + --pst-color-inline-code: rgb(221, 158, 194); + + // targeted content + --pst-color-target: rgb(71, 39, 0); + + // hide any content that should not be displayed in the dark theme + .only-light { + display: none !important; + } + + // specific brightness applied on images + img { + filter: brightness(0.8) contrast(1.2); + } +} + + +.bg-light { + background-color:rgba(80, 164, 171, 0.52)!important +} + diff --git a/_static/design-tabs.js b/_static/design-tabs.js new file mode 100644 index 00000000..b25bd6a4 --- /dev/null +++ b/_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record