From ab06473c3276cb4cbb6861f1699d3a15d5c89d59 Mon Sep 17 00:00:00 2001 From: munrojm Date: Mon, 8 Jan 2024 14:40:55 -0800 Subject: [PATCH 1/6] Initial commit of blessed tasks method --- mp_api/client/routes/materials/materials.py | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/mp_api/client/routes/materials/materials.py b/mp_api/client/routes/materials/materials.py index 7df557efc..2d7be86fd 100644 --- a/mp_api/client/routes/materials/materials.py +++ b/mp_api/client/routes/materials/materials.py @@ -2,6 +2,7 @@ from emmet.core.settings import EmmetSettings from emmet.core.symmetry import CrystalSystem +from emmet.core.vasp.calc_types import RunType from emmet.core.vasp.material import MaterialsDoc from pymatgen.core.structure import Structure @@ -318,3 +319,51 @@ def find_structure( return material_ids # type: ignore return material_ids[0] + + def get_blessed_calcs( + self, + run_type: RunType = RunType.R2SCAN, + material_ids: Optional[list[str]] = None, + uncorrected_energy: Optional[ + tuple[Optional[float], Optional[float]] | float + ] = None, + num_chunks: int | None = None, + chunk_size: int = 1000, + ): + """Get blessed calculation entries for a given material and run type. + + Args: + run_type (RunType): Calculation run type (e.g. GGA, GGA+U, R2SCAN, PBESol) + material_ids (list[str]): List of material ID values + uncorrected_energy (tuple[Optional[float], Optional[float]] | float): Tuple of minimum and maximum uncorrected DFT energy in eV/atom. + Note that if a single value is passed, it will be used as the minimum and maximum. + 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. + """ + query_params: dict = {"run_type": str(run_type)} + if material_ids: + if isinstance(material_ids, str): + material_ids = [material_ids] + + query_params.update({"material_ids": ",".join(validate_ids(material_ids))}) + + if uncorrected_energy: + if isinstance(uncorrected_energy, float): + uncorrected_energy = (uncorrected_energy, uncorrected_energy) + + query_params.update( + { + "uncorrected_energy_min": uncorrected_energy[0], # type: ignore + "uncorrected_energy_max": uncorrected_energy[1], # type: ignore + } + ) + + results = self._query_resource( + query_params, + # fields=["material_ids", "entries"], + suburl="blessed_tasks", + parallel_param="material_ids" if material_ids else None, + chunk_size=chunk_size, + num_chunks=num_chunks, + ) + return results.get("data") From 46301149804216e89c72525507e038ce2055824e Mon Sep 17 00:00:00 2001 From: munrojm Date: Wed, 10 Jan 2024 07:45:16 -0800 Subject: [PATCH 2/6] Function name change --- mp_api/client/routes/materials/materials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mp_api/client/routes/materials/materials.py b/mp_api/client/routes/materials/materials.py index 2d7be86fd..f8b6a3227 100644 --- a/mp_api/client/routes/materials/materials.py +++ b/mp_api/client/routes/materials/materials.py @@ -320,7 +320,7 @@ def find_structure( return material_ids[0] - def get_blessed_calcs( + def get_blessed_entries( self, run_type: RunType = RunType.R2SCAN, material_ids: Optional[list[str]] = None, From 21517d06aa79f1ec2e3891b46ad94b474ae3e01f Mon Sep 17 00:00:00 2001 From: Jason Munro Date: Thu, 29 Feb 2024 10:07:59 -0800 Subject: [PATCH 3/6] Linting --- mp_api/client/routes/materials/materials.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mp_api/client/routes/materials/materials.py b/mp_api/client/routes/materials/materials.py index f8b6a3227..2ab380852 100644 --- a/mp_api/client/routes/materials/materials.py +++ b/mp_api/client/routes/materials/materials.py @@ -323,10 +323,8 @@ def find_structure( def get_blessed_entries( self, run_type: RunType = RunType.R2SCAN, - material_ids: Optional[list[str]] = None, - uncorrected_energy: Optional[ - tuple[Optional[float], Optional[float]] | float - ] = None, + material_ids: list[str] | None = None, + uncorrected_energy: tuple[float | None, float | None] | float | None = None, num_chunks: int | None = None, chunk_size: int = 1000, ): From f307e636bd647a0247a8671ad29bfce9ad4b65a7 Mon Sep 17 00:00:00 2001 From: esoteric-ephemera Date: Thu, 8 Jan 2026 14:26:13 -0800 Subject: [PATCH 4/6] fix blessed entries rester --- mp_api/client/routes/materials/materials.py | 52 +++++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/mp_api/client/routes/materials/materials.py b/mp_api/client/routes/materials/materials.py index 2ab380852..181ce59f6 100644 --- a/mp_api/client/routes/materials/materials.py +++ b/mp_api/client/routes/materials/materials.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from emmet.core.settings import EmmetSettings from emmet.core.symmetry import CrystalSystem from emmet.core.vasp.calc_types import RunType -from emmet.core.vasp.material import MaterialsDoc +from emmet.core.vasp.material import MaterialsDoc, BlessedCalcs from pymatgen.core.structure import Structure from mp_api.client.core import BaseRester, MPRestError @@ -38,6 +40,10 @@ XASRester, ) +if TYPE_CHECKING: + from typing import Any + from pymatgen.entries.computed_entries import ComputedStructureEntry + _EMMET_SETTINGS = EmmetSettings() # type: ignore @@ -322,23 +328,30 @@ def find_structure( def get_blessed_entries( self, - run_type: RunType = RunType.R2SCAN, + run_type: str | RunType = RunType.r2SCAN, material_ids: list[str] | None = None, uncorrected_energy: tuple[float | None, float | None] | float | None = None, num_chunks: int | None = None, chunk_size: int = 1000, - ): + ) -> list[dict[str, str | dict | ComputedStructureEntry]]: """Get blessed calculation entries for a given material and run type. Args: - run_type (RunType): Calculation run type (e.g. GGA, GGA+U, R2SCAN, PBESol) + run_type (str or RunType): Calculation run type (e.g. GGA, GGA+U, r2SCAN, PBESol) material_ids (list[str]): List of material ID values uncorrected_energy (tuple[Optional[float], Optional[float]] | float): Tuple of minimum and maximum uncorrected DFT energy in eV/atom. Note that if a single value is passed, it will be used as the minimum and maximum. 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. + + Returns: + list of dict, of the form: + { + "material_id": MPID, + "blessed_entry": ComputedStructureEntry + } """ - query_params: dict = {"run_type": str(run_type)} + query_params: dict[str, Any] = {"run_type": str(run_type)} if material_ids: if isinstance(material_ids, str): material_ids = [material_ids] @@ -351,17 +364,36 @@ def get_blessed_entries( query_params.update( { - "uncorrected_energy_min": uncorrected_energy[0], # type: ignore - "uncorrected_energy_max": uncorrected_energy[1], # type: ignore + "uncorrected_energy_min": uncorrected_energy[0], + "uncorrected_energy_max": uncorrected_energy[1], } ) results = self._query_resource( query_params, - # fields=["material_ids", "entries"], + fields=["material_id", "entries"], suburl="blessed_tasks", parallel_param="material_ids" if material_ids else None, chunk_size=chunk_size, num_chunks=num_chunks, - ) - return results.get("data") + ) + + return [ + { + "material_id": doc["material_id"], + "blessed_entry": ( + next( + getattr(doc["entries"],k,None) + for k in BlessedCalcs.model_fields + if getattr(doc["entries"],k,None) + ) + if self.use_document_model + else next( + doc["entries"][k] + for k in BlessedCalcs.model_fields + if doc["entries"].get(k) + ) + ), + } + for doc in (results.get("data") or []) + ] From f45e4c298bb6d6cb86a054d4236a1b9113b2d543 Mon Sep 17 00:00:00 2001 From: esoteric-ephemera Date: Thu, 8 Jan 2026 14:26:28 -0800 Subject: [PATCH 5/6] precommit --- mp_api/client/routes/materials/materials.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mp_api/client/routes/materials/materials.py b/mp_api/client/routes/materials/materials.py index 181ce59f6..2f112b2fb 100644 --- a/mp_api/client/routes/materials/materials.py +++ b/mp_api/client/routes/materials/materials.py @@ -5,7 +5,7 @@ from emmet.core.settings import EmmetSettings from emmet.core.symmetry import CrystalSystem from emmet.core.vasp.calc_types import RunType -from emmet.core.vasp.material import MaterialsDoc, BlessedCalcs +from emmet.core.vasp.material import BlessedCalcs, MaterialsDoc from pymatgen.core.structure import Structure from mp_api.client.core import BaseRester, MPRestError @@ -42,6 +42,7 @@ if TYPE_CHECKING: from typing import Any + from pymatgen.entries.computed_entries import ComputedStructureEntry _EMMET_SETTINGS = EmmetSettings() # type: ignore @@ -376,16 +377,16 @@ def get_blessed_entries( parallel_param="material_ids" if material_ids else None, chunk_size=chunk_size, num_chunks=num_chunks, - ) - + ) + return [ { "material_id": doc["material_id"], "blessed_entry": ( next( - getattr(doc["entries"],k,None) + getattr(doc["entries"], k, None) for k in BlessedCalcs.model_fields - if getattr(doc["entries"],k,None) + if getattr(doc["entries"], k, None) ) if self.use_document_model else next( From 3b87f945d549eba785350c227b13cce5a3745f2e Mon Sep 17 00:00:00 2001 From: esoteric-ephemera Date: Thu, 8 Jan 2026 14:41:17 -0800 Subject: [PATCH 6/6] tests for blessed entries --- mp_api/client/routes/materials/materials.py | 4 ++-- tests/materials/test_materials.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mp_api/client/routes/materials/materials.py b/mp_api/client/routes/materials/materials.py index 2f112b2fb..bb200fbb1 100644 --- a/mp_api/client/routes/materials/materials.py +++ b/mp_api/client/routes/materials/materials.py @@ -365,8 +365,8 @@ def get_blessed_entries( query_params.update( { - "uncorrected_energy_min": uncorrected_energy[0], - "uncorrected_energy_max": uncorrected_energy[1], + "energy_min": uncorrected_energy[0], + "energy_max": uncorrected_energy[1], } ) diff --git a/tests/materials/test_materials.py b/tests/materials/test_materials.py index 09e1e80d5..76d2f1751 100644 --- a/tests/materials/test_materials.py +++ b/tests/materials/test_materials.py @@ -62,3 +62,24 @@ def test_client(rester): custom_field_tests=custom_field_tests, sub_doc_fields=sub_doc_fields, ) + + +@pytest.mark.xfail(condition=True, reason="Needs new deployment.", strict=False) +@pytest.mark.parametrize( + "run_type, uncorrected_energy, use_document_model", + [("PBE", None, True), ("r2SCAN", 1.0, False), ("GGA_U", (-50e4, 0.0), True)], +) +def test_blessed_entry(run_type, uncorrected_energy, use_document_model): + # Si and NiO. Si has GGA and r2SCAN entries, NiO has GGA, GGA+U, and r2SCAN + with MaterialsRester(use_document_model=use_document_model) as rester: + blessed = rester.get_blessed_entries( + run_type, + material_ids=["mp-149", "mp-19009"], + uncorrected_energy=uncorrected_energy, + ) + + assert all( + isinstance(entry, dict) + and all(entry.get(k) is not None for k in ("material_id", "blessed_entry")) + for entry in blessed + )