from __future__ import annotations
from collections import defaultdict
from typing import TYPE_CHECKING
from emmet.core.band_theory import BSPathType
from emmet.core.mpid import AlphaID
from emmet.core.phonon import PhononBS, PhononBSDOSDoc, PhononBSDOSTask, PhononDOS
from mp_api.client.core import BaseRester, MPRestError
from mp_api.client.core.utils import validate_ids
from mp_api.client.routes.materials.summary import SummaryRester
if TYPE_CHECKING:
from typing import Any
from emmet.core.math import Matrix3D
[docs]
class PhononRester(BaseRester):
_summary_rester: SummaryRester | None = None
suffix = "materials/phonon"
document_model = PhononBSDOSTask # type: ignore
primary_key = "identifier"
@property
def summary_rester(self) -> SummaryRester:
if not self._summary_rester:
self._summary_rester = SummaryRester(
api_key=self.api_key,
endpoint=self.base_endpoint,
include_user_agent=self.include_user_agent,
session=self.session,
use_document_model=self.use_document_model,
headers=self.headers,
mute_progress_bars=self.mute_progress_bars,
)
return self._summary_rester
[docs]
def search(
self,
identifiers: str | list[str] | None = None,
material_ids: str | list[str] | None = None,
phonon_method: str | None = None,
num_chunks: int | None = None,
chunk_size: int = 1000,
all_fields: bool = True,
fields: list[str] | None = None,
) -> list[PhononBSDOSTask] | list[dict]:
"""Query phonon docs using a variety of search criteria.
Exactly one of `identifiers` and `material_ids` may be supplied.
When `material_ids` is supplied, the summary endpoint is queried to
resolve each material's `phonon_IDs` into the underlying phonon task
identifiers (filtered by `phonon_method` when provided), and those
phonon IDs are then used to query the phonon endpoint.
Arguments:
identifiers (str, List[str]): A single Phonon Task ID string or list of strings
(e.g., aaaaaaft, [aaaaaaft, aaaeguxu]).
material_ids (str, List[str]): A single Materials Project ID or list of IDs
(e.g., mp-149, [mp-149, mp-13]).
phonon_method (str): phonon method to search (dfpt, phonopy, pheasy)
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 PhononBSDOSTask to return data for.
Default is identifier, last_updated, and formula_pretty if all_fields is False.
Returns:
([PhononBSDOSTask], [dict]) List of phonon documents or dictionaries.
"""
if identifiers and material_ids:
raise MPRestError(
"Specify exactly one of `identifiers` or `material_ids`, not both."
)
query_params: dict = defaultdict(dict)
if material_ids:
material_ids = (
[material_ids] if isinstance(material_ids, str) else list(material_ids)
)
summary_docs = self.summary_rester.search(
material_ids=material_ids, fields=["material_id", "phonon_IDs"]
)
resolved_ids: list[str] = []
missing: list[str] = []
found_material_ids: set[str] = set()
for doc in summary_docs:
mid = str(doc["material_id"]) # type: ignore[arg-type, index]
phonon_ids_by_method = doc.get("phonon_IDs") or {} # type: ignore[union-attr]
if phonon_method:
method_ids = phonon_ids_by_method.get(phonon_method) or []
else:
method_ids = [
pid for ids in phonon_ids_by_method.values() for pid in ids
]
if not method_ids:
missing.append(mid)
continue
found_material_ids.add(mid)
resolved_ids.extend(method_ids)
missing.extend(mid for mid in material_ids if mid not in found_material_ids)
if missing:
method_suffix = (
f" with phonon_method={phonon_method!r}" if phonon_method else ""
)
raise MPRestError(
f"No phonon data found for material ID(s) {sorted(set(missing))}"
f"{method_suffix}."
)
identifiers = resolved_ids
if identifiers:
query_params["identifiers"] = ",".join(
validate_ids(
[identifiers] if isinstance(identifiers, str) else identifiers
)
)
if phonon_method and phonon_method in {"dfpt", "phonopy", "pheasy"}:
query_params["phonon_method"] = phonon_method
query_params = {
entry: query_params[entry]
for entry in query_params
if query_params[entry] is not None
}
return super()._search( # type: ignore[return-value]
num_chunks=num_chunks,
chunk_size=chunk_size,
all_fields=all_fields,
fields=fields,
**query_params,
)
[docs]
def get_bandstructure_from_phonon_id(
self,
identifier: str,
phonon_method: str,
path_type: str | BSPathType = BSPathType.setyawan_curtarolo,
) -> PhononBS | dict[str, Any]:
"""Get the phonon band structure pymatgen object associated with a given phonon ID and phonon method.
Arguments:
identifier (str): Phonon ID for the phonon band structure calculation
phonon_method (str): phonon method, i.e. pheasy or dfpt
path_type (BSPathType or str): k-path selection convention for the band structure.
Returns:
bandstructure (PhononBS): PhononBS object
"""
ph_bs_lbl, _ = self._get_delta_table(
"materialsproject-parsed",
"phonon/electronic-structure/bandstructures/",
label="ph_bandstructure",
)
query = f"""
SELECT *
FROM {ph_bs_lbl}
WHERE identifier='{str(AlphaID(identifier.split("-")[-1],padlen=8))}'
AND phonon_method='{phonon_method}'
"""
if path_type:
query += f"\nAND path_convention='{path_type}'"
table = self._query_delta_single(query)
deser = table.to_pylist(maps_as_pydicts="strict")
if deser and deser[0].get("bandstructure") is not None:
bs = deser[0]["bandstructure"]
return PhononBS(**bs) if self.use_document_model else bs
raise MPRestError(
f"No phonon bandstructure data found for {identifier=} and {phonon_method=}"
+ (f" and run_type={path_type}" if path_type else "")
)
[docs]
def get_bandstructure_from_material_id(
self,
material_id: str,
phonon_method: str,
path_type: str | BSPathType = BSPathType.setyawan_curtarolo,
) -> PhononBS | dict[str, Any]:
"""Get the phonon band structure pymatgen object associated with a given material ID and phonon method.
Arguments:
material_id (str): Materials Project ID for a material
phonon_method (str): phonon method, i.e. pheasy or dfpt
path_type (BSPathType or str): k-path selection convention for the band structure.
Returns:
bandstructure (PhononBS): PhononBS object
"""
pt: BSPathType = (
BSPathType(path_type) if isinstance(path_type, str) else path_type
)
if not (
summary_doc := self.summary_rester.search(
material_ids=material_id, fields=["phonon_IDs"]
)
):
raise MPRestError(
f"No phonon bandstructure data found for material ID {material_id}."
)
if phonon_method not in summary_doc[0]["phonon_IDs"]: # type: ignore[arg-type, index]
raise MPRestError(
f"No phonon bandstructure data found for material ID: {material_id} and phonon method: {phonon_method}."
)
return self.get_bandstructure_from_phonon_id(
summary_doc[0]["phonon_IDs"][phonon_method][0], phonon_method, pt # type: ignore[arg-type, index]
)
[docs]
def get_dos_from_phonon_id(
self, identifier: str, phonon_method: str
) -> PhononDOS | dict[str, Any]:
"""Get the phonon dos pymatgen object associated with a given phonon ID and phonon method.
Arguments:
identifier (str): Phonon ID for the phonon dos calculation
phonon_method (str): phonon method, i.e. pheasy or dfpt
Returns:
dos (PhononDOS): PhononDOS object
"""
ph_dos_lbl, _ = self._get_delta_table(
"materialsproject-parsed",
"phonon/electronic-structure/dos/",
label="ph_dos",
)
query = f"""
SELECT *
FROM {ph_dos_lbl}
WHERE identifier='{str(AlphaID(identifier.split("-")[-1],padlen=8))}'
AND phonon_method='{phonon_method}'
"""
table = self._query_delta_single(query)
deser = table.to_pylist(maps_as_pydicts="strict")
if deser and deser[0].get("dos") is not None:
dos = deser[0]["dos"]
return PhononDOS(**dos) if self.use_document_model else dos
raise MPRestError(
f"No phonon dos data found for {identifier=} and {phonon_method=}"
)
[docs]
def get_dos_from_material_id(
self, material_id: str, phonon_method: str
) -> PhononDOS | dict[str, Any]:
"""Get the phonon dos pymatgen object associated with a given material ID and phonon method.
Arguments:
material_id (str): Materials Project ID for a material
phonon_method (str): phonon method, i.e. pheasy or dfpt
Returns:
dos (PhononDOS): PhononDOS object
"""
if not (
summary_doc := self.summary_rester.search(
material_ids=material_id, fields=["phonon_IDs"]
)
):
raise MPRestError(
f"No phonon dos data found for material ID {material_id}."
)
if phonon_method not in summary_doc[0]["phonon_IDs"]: # type: ignore[arg-type, index]
raise MPRestError(
f"No phonon dos data found for material ID: {material_id} and phonon method: {phonon_method}."
)
return self.get_dos_from_phonon_id(
summary_doc[0]["phonon_IDs"][phonon_method][0], phonon_method # type: ignore[arg-type, index]
)
[docs]
def get_forceconstants_from_phonon_id(
self, identifier: str, phonon_method: str
) -> list[list[Matrix3D]]:
"""Get the force constants associated with a given phonon ID and phonon method.
Arguments:
identifier (str): Phonon ID for the force constants calculation
phonon_method (str): phonon method, i.e. pheasy or dfpt
Returns:
force constants (list[list[Matrix3D]]): force constants
"""
ph_fc_lbl, _ = self._get_delta_table(
"materialsproject-parsed",
"phonon/force-constants/",
label="ph_force_constants",
)
query = f"""
SELECT *
FROM {ph_fc_lbl}
WHERE identifier='{str(AlphaID(identifier.split("-")[-1],padlen=8))}'
AND phonon_method='{phonon_method}'
"""
table = self._query_delta_single(query)
deser = table.to_pylist(maps_as_pydicts="strict")
if deser and deser[0].get("force_constants") is not None:
return deser[0]["force_constants"]
raise MPRestError(
f"No phonon force constants data found for {identifier=} and {phonon_method=}"
)
[docs]
def get_forceconstants_from_material_id(
self, material_id: str, phonon_method: str
) -> list[list[Matrix3D]]:
"""Get the force constants associated with a given material ID and phonon method.
Arguments:
material_id (str): Materials Project ID for a material
phonon_method (str): phonon method, i.e. pheasy or dfpt
Returns:
force constants (list[list[Matrix3D]]): force constants
"""
if not (
summary_doc := self.summary_rester.search(
material_ids=material_id, fields=["phonon_IDs"]
)
):
raise MPRestError(
f"No phonon force constants data found for material ID {material_id}."
)
if phonon_method not in summary_doc[0]["phonon_IDs"]: # type: ignore[arg-type, index]
raise MPRestError(
f"No phonon force constants data found for material ID: {material_id} and phonon method: {phonon_method}."
)
return self.get_forceconstants_from_phonon_id(
summary_doc[0]["phonon_IDs"][phonon_method][0], phonon_method # type: ignore[arg-type, index]
)
[docs]
def compute_thermo_quantities(
self,
material_id: str | None = None,
phonon_method: str | None = None,
identifier: str | None = None,
):
"""Compute thermodynamical quantities for a given phonon ID or material ID and phonon_method.
Exactly one of `identifier` or `material_id` must be supplied.
Arguments:
identifier (str): Phonon ID to calculate quantities for
material_id (str): Materials Project ID to calculate quantities for; the first
phonon ID associated with the requested `phonon_method` for the material
will be used.
phonon_method (str): phonon method, i.e. pheasy or dfpt
Returns:
quantities (dict): thermodynamical quantities
"""
if identifier and material_id:
raise MPRestError(
"Specify exactly one of `identifier` or `material_id`, not both."
)
if not identifier and not material_id:
raise MPRestError("One of `identifier` or `material_id` must be specified.")
use_document_model = self.use_document_model
self.use_document_model = False
try:
if material_id:
if not (
summary_doc := self.summary_rester.search(
material_ids=material_id, fields=["phonon_IDs"]
)
):
raise MPRestError(
f"No phonon data found for material ID {material_id}."
)
if phonon_method not in summary_doc[0]["phonon_IDs"]: # type: ignore[arg-type, index]
raise MPRestError(
f"No phonon data found for material ID: {material_id} and "
f"phonon method: {phonon_method}."
)
identifier = summary_doc[0]["phonon_IDs"][phonon_method][0] # type: ignore[arg-type, index]
if identifier is None or phonon_method is None:
suffix = (
f" for {material_id=}" if material_id else f" for {identifier=}"
)
raise MPRestError(
f"Could not resolve a phonon identifier or method{suffix} "
f"({phonon_method=})."
)
ph_dos = self.get_dos_from_phonon_id(identifier, phonon_method)
docs = self.search(identifiers=identifier, phonon_method=phonon_method)
if not docs or not docs[0]:
raise MPRestError("No phonon document found")
self.use_document_model = True
docs[0]["phonon_dos"] = ph_dos # type: ignore[index]
doc = PhononBSDOSDoc(**docs[0]) # type: ignore[arg-type, index]
finally:
self.use_document_model = use_document_model
# below: same as numpy.linspace(0,800,100) but written out for mypy
return doc.compute_thermo_quantities([i * 800 / 99 for i in range(100)])