diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 00000000..e12cbcf4 --- /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: e115b12a6e1950854652473c17e39d56 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/about/changelog.doctree b/.doctrees/about/changelog.doctree new file mode 100644 index 00000000..de6b0bde 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..592dc954 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..cb2a0ba1 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..c848c0c1 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..2604e1ea 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..899dfb82 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..d97508ab 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..e58a98a4 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..0301820e 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..ec4fb0fd 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..d8289eb4 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..388f9cb6 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..558b2d0e 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..a89dd737 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..18218026 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..0e05d7b4 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..01db629e 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..d6d2cb10 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..0d15acb1 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..e1a31b7d 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..e97b6382 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..07336f88 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..9d3e8888 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..d57e9ea3 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..0d8ba054 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..11a049cd 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..8e0d68b5 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..fd46b658 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..ced5d1a0 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..09cd779c 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..0a97140a 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..7b646fea 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..d3c0e249 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..846009ce 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..42ffd592 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..d6ff124d 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..422f8deb 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..f4d9cba0 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..911cf3fe 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..2b9031c8 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..2696e747 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..8963c024 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..efd0909e 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..03fbce44 Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.TYPE_CHECKING.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.base_style.doctree b/.doctrees/reference/lobsterpy.plotting.base_style.doctree new file mode 100644 index 00000000..02f1620f Binary files /dev/null and b/.doctrees/reference/lobsterpy.plotting.base_style.doctree differ diff --git a/.doctrees/reference/lobsterpy.plotting.doctree b/.doctrees/reference/lobsterpy.plotting.doctree new file mode 100644 index 00000000..a1b79e3e 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..14f4530a 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..dde6c0a6 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..88f43679 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..88ee91da 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..231a3a05 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..3cf9cc77 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..9db29975 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..0a7e111f 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..21f36583 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..da04a4a4 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..be16d9b0 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..3ebd8e6d 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..8da2c6bc 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..70528a3d 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..906c45a2 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..b4324fd1 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..d1aac24a 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..7fe4e694 --- /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 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]
+ 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 available_orbitals:
+ cohp_summed_orb = self.chemenv.completecohp.get_summed_cohp_by_label_and_orbital_list(
+ label_list=bond_labels, orbital_list=[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:
+ orb_icohp_bn = self.chemenv.Icohpcollection.get_icohp_by_label(
+ label=bond_label, orbitals=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:
+ orb_icohp_an = self.chemenv.Icohpcollection.get_icohp_by_label(
+ label=bond_label, orbitals=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],
+ },
+ }
+
+ # 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],
+ },
+ }
+
+ 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': {'2s-3s': 0.68},
+ 'max_antibonding_contribution': {'2s-2pz': 0.36}}}
+
+ """
+ # 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': {'3s-3s': ['21', '23', '24', '27', '28', '30']}
+
+ """
+ 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]
+ orb_plot_data[key].update({orb_pair: label_list})
+ 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., "2px-3s"
+
+ Returns:
+ will return list of str, e.g. ["Na(2px)", "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."
+ )
+
+ 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
+
+[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."
+ )
+
+ 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"
+ )
+ 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"
+ )
+ 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"
+ )
+ 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"
+ )
+ 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."
+ )
+
+ 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
+
+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
+
+ 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 not hide:
+ plot.show()
+ else:
+ if not save:
+ plot.close()
+
+[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=["poscar", "cohpcar", "icohplist"]
+ )
+ structure_path = file_paths.get("poscar")
+
+ 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=["poscar", "charge"])
+
+ if self.charge_type == "mulliken":
+ charge_mull = FeaturizeCharges(
+ path_to_charge=str(file_paths.get("charge")),
+ path_to_structure=str(file_paths.get("poscar")),
+ 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("poscar")),
+ 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("poscar")),
+ 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("poscar")),
+ 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=["poscar", "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=["poscar", "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=["poscar", "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("poscar")),
+ 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", "poscar"],
+ )
+
+ graph = LobsterGraph(
+ path_to_poscar=str(file_paths.get("poscar")),
+ 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=["poscar", "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("poscar")),
+ 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=["poscar", "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("poscar")),
+ 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=["poscar", "cohpcar", "icohplist", "charge"]
+ )
+
+ which_bonds = bonds.replace("-", "_")
+ bond_type = f"{which_bonds}_bonds"
+
+ try:
+ analyse = Analysis(
+ path_to_poscar=str(file_paths.get("poscar")),
+ 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"
+ )
+ 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 = {
+ "poscar": "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"),
+ }
+
+ 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 == "poscar":
+ try:
+ file_paths[file] = get_structure_path(lobster_path=lobster_path)
+ except Exception:
+ missing_files.append(default_values["poscar"])
+ 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 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 pkg_resources import resource_filename
+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
+
+base_style = resource_filename("lobsterpy.plotting", "lobsterpy_base.mplstyle")
+
+
+[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 [base_style]
+ 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:
+ cohp_orb = complete_cohp.get_summed_cohp_by_label_and_orbital_list(
+ label_list=[lab], orbital_list=[orb]
+ )
+
+ 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():
+ cohp_orb = complete_cohp.get_summed_cohp_by_label_and_orbital_list(
+ label_list=val, orbital_list=[orb] * len(val)
+ )
+
+ 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,
+ 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-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 00000000..eb19f698 --- /dev/null +++ b/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/_static/design-tabs.js b/_static/design-tabs.js new file mode 100644 index 00000000..36b38cf0 --- /dev/null +++ b/_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 00000000..527b876c --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 00000000..05f76e70 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/images/logo_binder.svg b/_static/images/logo_binder.svg new file mode 100644 index 00000000..45fecf75 --- /dev/null +++ b/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + diff --git a/_static/images/logo_colab.png b/_static/images/logo_colab.png new file mode 100644 index 00000000..b7560ec2 Binary files /dev/null and b/_static/images/logo_colab.png differ diff --git a/_static/images/logo_deepnote.svg b/_static/images/logo_deepnote.svg new file mode 100644 index 00000000..fa77ebfc --- /dev/null +++ b/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/_static/images/logo_jupyterhub.svg b/_static/images/logo_jupyterhub.svg new file mode 100644 index 00000000..60cfe9f2 --- /dev/null +++ b/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ + diff --git a/_static/jquery-3.6.0.js b/_static/jquery-3.6.0.js new file mode 100644 index 00000000..fc6c299b --- /dev/null +++ b/_static/jquery-3.6.0.js @@ -0,0 +1,10881 @@ +/*! + * jQuery JavaScript Library v3.6.0 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2021-03-02T17:08Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML