from __future__ import annotations
import warnings
from collections import defaultdict
from emmet.core.electronic_structure import (
BSPathType,
DOSProjectionType,
ElectronicStructureDoc,
)
from pymatgen.analysis.magnetism.analyzer import Ordering
from pymatgen.core.periodic_table import Element
from pymatgen.electronic_structure.core import OrbitalType, Spin
from mp_api.client.core import BaseRester, MPRestError
from mp_api.client.core.utils import validate_ids
[docs]
class ElectronicStructureRester(BaseRester[ElectronicStructureDoc]):
suffix = "materials/electronic_structure"
document_model = ElectronicStructureDoc # type: ignore
primary_key = "material_id"
[docs]
def search_electronic_structure_docs(self, *args, **kwargs): # pragma: no cover
"""Deprecated."""
warnings.warn(
"MPRester.electronic_structure.search_electronic_structure_docs is deprecated. "
"Please use MPRester.electronic_structure.search instead.",
DeprecationWarning,
stacklevel=2,
)
return self.search(*args, **kwargs)
[docs]
def search(
self,
material_ids: str | list[str] | None = None,
band_gap: tuple[float, float] | None = None,
chemsys: str | list[str] | None = None,
efermi: tuple[float, float] | None = None,
elements: list[str] | None = None,
exclude_elements: list[str] | None = None,
formula: str | list[str] | None = None,
is_gap_direct: bool = None,
is_metal: bool = None,
magnetic_ordering: Ordering | None = None,
num_elements: tuple[int, int] | None = None,
num_chunks: int | None = None,
chunk_size: int = 1000,
all_fields: bool = True,
fields: list[str] | None = None,
):
"""Query electronic structure docs using a variety of search criteria.
Arguments:
material_ids (str, List[str]): A single Material ID string or list of strings
(e.g., mp-149, [mp-149, mp-13]).
band_gap (Tuple[float,float]): Minimum and maximum band gap in eV to consider.
chemsys (str, List[str]): A chemical system or list of chemical systems
(e.g., Li-Fe-O, Si-*, [Si-O, Li-Fe-P]).
efermi (Tuple[float,float]): Minimum and maximum fermi energy in eV to consider.
elements (List[str]): A list of elements.
exclude_elements (List[str]): A list of elements to exclude.
formula (str, List[str]): A formula including anonymized formula
or wild cards (e.g., Fe2O3, ABO3, Si*). A list of chemical formulas can also be passed
(e.g., [Fe2O3, ABO3]).
is_gap_direct (bool): Whether the material has a direct band gap.
is_metal (bool): Whether the material is considered a metal.
magnetic_ordering (Ordering): Magnetic ordering of the material.
num_elements (Tuple[int,int]): Minimum and maximum number of elements to consider.
num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible.
chunk_size (int): Number of data entries per chunk.
all_fields (bool): Whether to return all fields in the document. Defaults to True.
fields (List[str]): List of fields in EOSDoc to return data for.
Default is material_id and last_updated if all_fields is False.
Returns:
([ElectronicStructureDoc]) List of electronic structure documents
"""
query_params = defaultdict(dict) # type: dict
if material_ids:
if isinstance(material_ids, str):
material_ids = [material_ids]
query_params.update({"material_ids": ",".join(validate_ids(material_ids))})
if formula:
if isinstance(formula, str):
formula = [formula]
query_params.update({"formula": ",".join(formula)})
if chemsys:
if isinstance(chemsys, str):
chemsys = [chemsys]
query_params.update({"chemsys": ",".join(chemsys)})
if elements:
query_params.update({"elements": ",".join(elements)})
if exclude_elements:
query_params.update({"exclude_elements": ",".join(exclude_elements)})
if band_gap:
query_params.update(
{"band_gap_min": band_gap[0], "band_gap_max": band_gap[1]}
)
if efermi:
query_params.update({"efermi_min": efermi[0], "efermi_max": efermi[1]})
if magnetic_ordering:
query_params.update({"magnetic_ordering": magnetic_ordering.value})
if num_elements:
if isinstance(num_elements, int):
num_elements = (num_elements, num_elements)
query_params.update(
{"nelements_min": num_elements[0], "nelements_max": num_elements[1]}
)
if is_gap_direct is not None:
query_params.update({"is_gap_direct": is_gap_direct})
if is_metal is not None:
query_params.update({"is_metal": is_metal})
query_params = {
entry: query_params[entry]
for entry in query_params
if query_params[entry] is not None
}
return super()._search(
num_chunks=num_chunks,
chunk_size=chunk_size,
all_fields=all_fields,
fields=fields,
**query_params,
)
[docs]
class BandStructureRester(BaseRester):
suffix = "materials/electronic_structure/bandstructure"
document_model = ElectronicStructureDoc # type: ignore
[docs]
def search_bandstructure_summary(self, *args, **kwargs): # pragma: no cover
"""Deprecated."""
warnings.warn(
"MPRester.electronic_structure_bandstructure.search_bandstructure_summary is deprecated. "
"Please use MPRester.electronic_structure_bandstructure.search instead.",
DeprecationWarning,
stacklevel=2,
)
return self.search(*args, **kwargs)
[docs]
def search(
self,
band_gap: tuple[float, float] | None = None,
efermi: tuple[float, float] | None = None,
is_gap_direct: bool = None,
is_metal: bool = None,
magnetic_ordering: Ordering | None = None,
path_type: BSPathType = BSPathType.setyawan_curtarolo,
num_chunks: int | None = None,
chunk_size: int = 1000,
all_fields: bool = True,
fields: list[str] | None = None,
):
"""Query band structure summary data in electronic structure docs using a variety of search criteria.
Arguments:
band_gap (Tuple[float,float]): Minimum and maximum band gap in eV to consider.
efermi (Tuple[float,float]): Minimum and maximum fermi energy in eV to consider.
is_gap_direct (bool): Whether the material has a direct band gap.
is_metal (bool): Whether the material is considered a metal.
magnetic_ordering (Ordering): Magnetic ordering of the material.
path_type (BSPathType): k-path selection convention for the band structure.
num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible.
chunk_size (int): Number of data entries per chunk.
all_fields (bool): Whether to return all fields in the document. Defaults to True.
fields (List[str]): List of fields in ElectronicStructureDoc to return data for.
Default is material_id and last_updated if all_fields is False.
Returns:
([ElectronicStructureDoc]) List of electronic structure documents
"""
query_params = defaultdict(dict) # type: dict
query_params["path_type"] = path_type.value
if band_gap:
query_params.update(
{"band_gap_min": band_gap[0], "band_gap_max": band_gap[1]}
)
if efermi:
query_params.update({"efermi_min": efermi[0], "efermi_max": efermi[1]})
if magnetic_ordering:
query_params.update({"magnetic_ordering": magnetic_ordering.value})
if is_gap_direct is not None:
query_params.update({"is_gap_direct": is_gap_direct})
if is_metal is not None:
query_params.update({"is_metal": is_metal})
query_params = {
entry: query_params[entry]
for entry in query_params
if query_params[entry] is not None
}
return super()._search(
num_chunks=num_chunks,
chunk_size=chunk_size,
all_fields=all_fields,
fields=fields,
**query_params,
)
[docs]
def get_bandstructure_from_task_id(self, task_id: str):
"""Get the band structure pymatgen object associated with a given task ID.
Arguments:
task_id (str): Task ID for the band structure calculation
Returns:
bandstructure (BandStructure): BandStructure or BandStructureSymmLine object
"""
result = self._query_open_data(
bucket="materialsproject-parsed", prefix="bandstructures", key=task_id
)
if result.get("data", None) is not None:
return result["data"]
else:
raise MPRestError("No object found")
[docs]
def get_bandstructure_from_material_id(
self,
material_id: str,
path_type: BSPathType = BSPathType.setyawan_curtarolo,
line_mode=True,
):
"""Get the band structure pymatgen object associated with a Materials Project ID.
Arguments:
material_id (str): Materials Project ID for a material
path_type (BSPathType): k-point path selection convention
line_mode (bool): Whether to return data for a line-mode calculation
Returns:
bandstructure (Union[BandStructure, BandStructureSymmLine]): BandStructure or BandStructureSymmLine object
"""
es_rester = ElectronicStructureRester(
endpoint=self.base_endpoint, api_key=self.api_key
)
if line_mode:
bs_data = es_rester.get_data_by_id(
document_id=material_id, fields=["bandstructure"]
).bandstructure
if bs_data is None:
raise MPRestError(
f"No {path_type.value} band structure data found for {material_id}"
)
else:
bs_data = bs_data.model_dump()
if bs_data.get(path_type.value, None):
bs_task_id = bs_data[path_type.value]["task_id"]
else:
raise MPRestError(
f"No {path_type.value} band structure data found for {material_id}"
)
else:
bs_data = es_rester.get_data_by_id(
document_id=material_id, fields=["dos"]
).dos
if bs_data is None:
raise MPRestError(
f"No uniform band structure data found for {material_id}"
)
else:
bs_data = bs_data.model_dump()
if bs_data.get("total", None):
bs_task_id = bs_data["total"]["1"]["task_id"]
else:
raise MPRestError(
f"No uniform band structure data found for {material_id}"
)
bs_obj = self.get_bandstructure_from_task_id(bs_task_id)
if bs_obj:
return bs_obj
else:
raise MPRestError("No band structure object found.")
[docs]
class DosRester(BaseRester):
suffix = "materials/electronic_structure/dos"
document_model = ElectronicStructureDoc # type: ignore
[docs]
def search_dos_summary(self, *args, **kwargs): # pragma: no cover
"""Deprecated."""
warnings.warn(
"MPRester.electronic_structure_dos.search_dos_summary is deprecated. "
"Please use MPRester.electronic_structure_dos.search instead.",
DeprecationWarning,
stacklevel=2,
)
return self.search(*args, **kwargs)
[docs]
def search(
self,
band_gap: tuple[float, float] | None = None,
efermi: tuple[float, float] | None = None,
element: Element | None = None,
magnetic_ordering: Ordering | None = None,
orbital: OrbitalType | None = None,
projection_type: DOSProjectionType = DOSProjectionType.total,
spin: Spin = Spin.up,
num_chunks: int | None = None,
chunk_size: int = 1000,
all_fields: bool = True,
fields: list[str] | None = None,
):
"""Query density of states summary data in electronic structure docs using a variety of search criteria.
Arguments:
band_gap (Tuple[float,float]): Minimum and maximum band gap in eV to consider.
efermi (Tuple[float,float]): Minimum and maximum fermi energy in eV to consider.
element (Element): Element for element-projected dos data.
magnetic_ordering (Ordering): Magnetic ordering of the material.
orbital (OrbitalType): Orbital for orbital-projected dos data.
projection_type (DOSProjectionType): Projection type of dos data. Default is the total dos.
spin (Spin): Spin channel of dos data. If non spin-polarized data is stored in Spin.up
num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible.
chunk_size (int): Number of data entries per chunk.
all_fields (bool): Whether to return all fields in the document. Defaults to True.
fields (List[str]): List of fields in EOSDoc to return data for.
Default is material_id and last_updated if all_fields is False.
Returns:
([ElectronicStructureDoc]) List of electronic structure documents
"""
query_params = defaultdict(dict) # type: dict
query_params["projection_type"] = projection_type.value
query_params["spin"] = spin.value
if element:
query_params["element"] = element.value
if orbital:
query_params["orbital"] = orbital.value
if band_gap:
query_params.update(
{"band_gap_min": band_gap[0], "band_gap_max": band_gap[1]}
)
if efermi:
query_params.update({"efermi_min": efermi[0], "efermi_max": efermi[1]})
if magnetic_ordering:
query_params.update({"magnetic_ordering": magnetic_ordering.value})
query_params = {
entry: query_params[entry]
for entry in query_params
if query_params[entry] is not None
}
return super()._search(
num_chunks=num_chunks,
chunk_size=chunk_size,
all_fields=all_fields,
fields=fields,
**query_params,
)
[docs]
def get_dos_from_task_id(self, task_id: str):
"""Get the density of states pymatgen object associated with a given calculation ID.
Arguments:
task_id (str): Task ID for the density of states calculation
Returns:
bandstructure (CompleteDos): CompleteDos object
"""
result = self._query_open_data(
bucket="materialsproject-parsed", prefix="dos", key=task_id
)
if result.get("data", None) is not None:
return result["data"]
else:
raise MPRestError("No object found")
[docs]
def get_dos_from_material_id(self, material_id: str):
"""Get the complete density of states pymatgen object associated with a Materials Project ID.
Arguments:
material_id (str): Materials Project ID for a material
Returns:
dos (CompleteDos): CompleteDos object
"""
es_rester = ElectronicStructureRester(
endpoint=self.base_endpoint, api_key=self.api_key
)
dos_data = es_rester.get_data_by_id(
document_id=material_id, fields=["dos"]
).model_dump()
if dos_data["dos"]:
dos_task_id = dos_data["dos"]["total"]["1"]["task_id"]
else:
raise MPRestError(f"No density of states data found for {material_id}")
dos_obj = self.get_dos_from_task_id(dos_task_id)
if dos_obj:
return dos_obj
else:
raise MPRestError("No density of states object found.")